diff --git a/.github/actions/prepare-release-benchmarks/action.yml b/.github/actions/prepare-release-benchmarks/action.yml index a5d6260..b7bda6f 100644 --- a/.github/actions/prepare-release-benchmarks/action.yml +++ b/.github/actions/prepare-release-benchmarks/action.yml @@ -25,10 +25,10 @@ runs: with: cache: false - - name: Validate benchmark inputs - shell: bash - run: just test-bench-inputs - - name: Inventory full release suites shell: bash run: just bench-release-inventory + + - name: Validate benchmark inputs + shell: bash + run: just test-bench-inputs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 354b81b..115a621 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -51,7 +51,9 @@ updates: # Enable version updates for Python development dependencies locked by uv - package-ecosystem: "uv" - directory: "/" + directories: + - "/" + - "/tooling/semgrep" schedule: interval: "weekly" day: "monday" diff --git a/.github/workflows/release-benchmarks.yml b/.github/workflows/release-benchmarks.yml index 53d4af9..3a7b3fe 100644 --- a/.github/workflows/release-benchmarks.yml +++ b/.github/workflows/release-benchmarks.yml @@ -31,10 +31,16 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 outputs: - release-id: ${{ steps.target.outputs.release-id }} - commit: ${{ steps.target.outputs.commit }} + target: ${{ steps.target.outputs.target }} validated-attempt: ${{ github.run_attempt }} steps: + - name: Install trusted package runner + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.13.1" + enable-cache: false + cache-python: false + - name: Validate draft release target id: target env: @@ -44,32 +50,14 @@ jobs: run: | set -euo pipefail - if [[ ! "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then - echo "::error::Expected an existing stable vX.Y.Z tag" - exit 1 - fi if [[ "$GITHUB_REF" != "refs/tags/$RELEASE_TAG" ]]; then echo "::error::Dispatch with --ref $RELEASE_TAG and the matching tag input" exit 1 fi - # The release-by-tag endpoint only guarantees published releases. - releases="$(gh api "repos/$GH_REPO/releases?per_page=100" --paginate --slurp)" - release_id="$(jq -er --arg tag "$RELEASE_TAG" ' - [.[][] | select(.tag_name == $tag)] - | if length == 1 and .[0].draft == true and .[0].prerelease == false - and .[0].immutable == false and .[0].name == $tag - then .[0].id else error("Expected exactly one mutable stable draft with the tag as its title") end - ' <<< "$releases")" - [[ "$release_id" =~ ^[1-9][0-9]*$ ]] || exit 1 - # Fully qualify the tag to avoid a same-named branch; peel annotated tags. - commit="$(gh api "repos/$GH_REPO/commits/refs/tags/$RELEASE_TAG" --jq .sha)" - [[ "$commit" =~ ^[0-9a-f]{40}$ ]] || exit 1 - if [[ "$commit" != "$GITHUB_SHA" ]]; then - echo "::error::Release tag no longer matches the workflow commit" - exit 1 - fi - echo "release-id=$release_id" >> "$GITHUB_OUTPUT" - echo "commit=$commit" >> "$GITHUB_OUTPUT" + target="$(uvx --python 3.15 --from research-repo-tools==0.1.9 research-repo-tools \ + --root "$RUNNER_TEMP" performance release-draft "$GH_REPO" "$RELEASE_TAG" \ + --commit "$GITHUB_SHA" --expected-title "$RELEASE_TAG" | jq -c .)" + echo "target=$target" >> "$GITHUB_OUTPUT" release-baseline: needs: validate-release @@ -182,6 +170,13 @@ jobs: timeout-minutes: 10 steps: + - name: Install trusted package runner + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.13.1" + enable-cache: false + cache-python: false + - name: Download release baseline uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -192,62 +187,16 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ inputs.tag }} - RELEASE_ID: ${{ needs.validate-release.outputs.release-id }} - RELEASE_COMMIT: ${{ needs.validate-release.outputs.commit }} + RELEASE_TARGET: ${{ needs.validate-release.outputs.target }} RELEASE_ASSET: ${{ needs.release-baseline.outputs.release-asset }} run: | set -euo pipefail - [[ "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || exit 1 - [[ "$RELEASE_ID" =~ ^[1-9][0-9]*$ ]] || exit 1 - [[ "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]] || exit 1 [[ "$RELEASE_ASSET" == "la-stack-${RELEASE_TAG}-criterion-baseline.tar.gz" ]] || exit 1 - [[ -f "$RELEASE_ASSET" && ! -L "$RELEASE_ASSET" && -s "$RELEASE_ASSET" ]] || exit 1 - digest="$(sha256sum "$RELEASE_ASSET")" - digest="sha256:${digest%% *}" - size="$(wc -c < "$RELEASE_ASSET")" - release_path="repos/$GH_REPO/releases/$RELEASE_ID" - - check_draft() { - gh api "$release_path" | jq -e --arg tag "$RELEASE_TAG" --argjson id "$RELEASE_ID" ' - .id == $id and .tag_name == $tag and .name == $tag - and .draft == true and .prerelease == false and .immutable == false - ' > /dev/null - local current_commit - current_commit="$(gh api "repos/$GH_REPO/commits/refs/tags/$RELEASE_TAG" --jq .sha)" - [[ "$current_commit" == "$RELEASE_COMMIT" ]] - } - matching_assets() { - gh api "$release_path/assets?per_page=100" --paginate --slurp | - jq --arg name "$RELEASE_ASSET" '[.[][] | select(.name == $name)]' - } - verify_asset() { - jq -e --arg digest "$digest" --argjson size "$size" ' - length == 1 and .[0].state == "uploaded" - and .[0].digest == $digest and .[0].size == $size - ' > /dev/null - } - - check_draft - assets="$(matching_assets)" - if [[ "$(jq length <<< "$assets")" == 0 ]]; then - # Address the captured release ID, never a replacement draft by tag. - gh api "https://uploads.github.com/$release_path/assets?name=$RELEASE_ASSET" \ - --method POST --header 'Content-Type: application/gzip' \ - --input "$RELEASE_ASSET" > /dev/null - elif ! verify_asset <<< "$assets"; then - echo "::error::Conflicting draft asset; inspect it before removing it and retrying" - exit 1 - fi - - # An interrupted upload can be reused only when its bytes match exactly. - # Recheck mutable state and tag after upload, before the irreversible step. - check_draft - matching_assets | verify_asset - gh api "$release_path" --method PATCH -F draft=false | - jq -e --arg tag "$RELEASE_TAG" --argjson id "$RELEASE_ID" ' - .id == $id and .tag_name == $tag and .draft == false and .prerelease == false - ' > /dev/null + printf '%s\n' "$RELEASE_TARGET" > "$RUNNER_TEMP/release-target.json" + uvx --python 3.15 --from research-repo-tools==0.1.9 research-repo-tools \ + --root "$RUNNER_TEMP" performance release-upload "$GH_REPO" "$RELEASE_TAG" \ + "$GITHUB_WORKSPACE/$RELEASE_ASSET" --target "$RUNNER_TEMP/release-target.json" --publish - name: Release baseline summary env: diff --git a/.github/workflows/semgrep-sarif.yml b/.github/workflows/semgrep-sarif.yml index 14d0e51..943c80b 100644 --- a/.github/workflows/semgrep-sarif.yml +++ b/.github/workflows/semgrep-sarif.yml @@ -41,7 +41,7 @@ jobs: id: semgrep run: | set +e - uv run --locked semgrep \ + uv run --locked --project tooling/semgrep semgrep \ --metrics off \ --error \ --strict \ diff --git a/.python-version b/.python-version index 6324d40..93a848f 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14 +3.15 diff --git a/AGENTS.md b/AGENTS.md index 9e0f8c5..d6e72f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -214,7 +214,7 @@ Favor the invariant over a convenient edit or faster implementation. ## Python -- Support tooling targets Python 3.14. Use `uv run --locked` for all +- Support tooling targets Python 3.15. Use `uv run --locked` for all Python scripts; never invoke `python` or `python3` directly. - Use pytest, not unittest, and add type hints to new code. - `just python-check` includes blocking type checking; all code must pass. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1acdd69..05f1903 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ change files. Run bare `just` for the complete generated command list, including arguments and descriptions. The [command policy](docs/dev/docs.md#command-discovery-and-placement) owns recipe naming and documentation placement. -Setup, tool checks, updates, changelog, and opt-in CodeRabbit review use the published `research-repo-tools==0.1.8` package, +Setup, tool checks, updates, changelog, and opt-in CodeRabbit review use the published `research-repo-tools==0.1.9` package, locked in the `tooling` dependency group and included by `dev`. Normal setup and CI install it from PyPI through `uv sync --locked --group dev`; a sibling checkout is unnecessary. To upgrade it deliberately, review the exact @@ -100,7 +100,8 @@ adapter and authentication regressions belong to research-repo-tools. Hosted CI runs a blocking finding check before a separate SARIF export; native SARIF output alone does not fail on findings. -`just security` runs the shared OSV dependency audit for `uv.lock` and `Cargo.lock`, +`just security` runs the shared OSV dependency audit for `uv.lock`, +`tooling/semgrep/uv.lock` and `Cargo.lock`, then the Gitleaks scan of reachable Git history and current tracked/nonignored files. Run either gate separately with `just audit` or `just security-secrets`. Setup installs both declared managed binaries. OSV needs @@ -157,6 +158,9 @@ CodeRabbit's successful status on a skipped bot review is not an approval. The [rollout guide](docs/dev/MANAGING_CHANGES.md#dependabot-approval-rollout) owns GitHub settings and post-merge verification. The workflow never executes PR code; dependency eligibility remains in `.github/dependabot.yml`. +Dependabot also monitors `tooling/semgrep/`; those scanner updates require manual +review because the trusted uv approval allowlist covers only the root manifest +and lockfile. ### Rust Toolchain Policy diff --git a/Cargo.lock b/Cargo.lock index 187510d..c521d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,18 +55,22 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bit-set" -version = "0.8.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" +dependencies = [ + "borsh", + "serde", +] [[package]] name = "bitflags" @@ -74,6 +78,30 @@ version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -97,7 +125,7 @@ checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -106,6 +134,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cast" version = "0.3.0" @@ -114,9 +148,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "shlex", @@ -128,6 +162,23 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -180,6 +231,21 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "criterion" version = "0.8.2" @@ -321,6 +387,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b14b339eb76d07f052cdbad76ca7c1310e56173a138095d3bf42a23c06ef5d8" +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + [[package]] name = "errno" version = "0.3.14" @@ -534,18 +606,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c81fb5260e37854d09d5c87183309fd8c555b75289427884b25660bc87a85e" -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - [[package]] name = "getrandom" version = "0.4.3" @@ -554,7 +614,8 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi 6.0.0", + "r-efi", + "rand_core", ] [[package]] @@ -594,6 +655,12 @@ dependencies = [ "zerocopy", ] +[[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" @@ -606,6 +673,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "interpol" version = "0.2.1" @@ -919,15 +996,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "private-gemm-x86" version = "0.1.22" @@ -940,6 +1008,15 @@ dependencies = [ "raw-cpuid", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -951,16 +1028,17 @@ dependencies = [ [[package]] name = "proptest" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" dependencies = [ "bit-set", "bit-vec", "bitflags", + "chacha20", + "core_detect", "num-traits", "rand", - "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -1018,12 +1096,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -1032,38 +1104,25 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ - "ppv-lite86", + "getrandom", "rand_core", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_xorshift" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" dependencies = [ "rand_core", ] @@ -1220,14 +1279,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -1284,9 +1343,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -1314,7 +1373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom", "once_cell", "rustix", "windows-sys", @@ -1350,6 +1409,36 @@ dependencies = [ "serde_json", ] +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + [[package]] name = "typenum" version = "1.20.1" @@ -1364,9 +1453,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "version_check" @@ -1393,15 +1482,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -1434,7 +1514,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -1514,25 +1594,28 @@ dependencies = [ ] [[package]] -name = "wit-bindgen" -version = "0.57.1" +name = "winnow" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] [[package]] name = "zerocopy" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 4dccba7..91a7845 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ allocation-counter = "0.8.1" approx = "0.5.1" criterion = { version = "0.8.2", features = [ "html_reports" ] } pastey = "0.2.3" -proptest = "1.11.0" +proptest = "1.12.0" [features] default = [ ] diff --git a/SECURITY.md b/SECURITY.md index b05babf..b0f4dd3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -118,11 +118,12 @@ assignment in that file. Other assignments and paths remain checked. ### Python tool dependencies -Semgrep 1.178.0 requires `pyjwt[crypto]~=2.13.0`, which retains packages affected -by the PyJWT advisories reported by OSV. The uv override in `pyproject.toml` -preserves the crypto extra and selects `pyjwt>=2.15.1,<3`; `uv.lock` records the -resolved version. Remove the override when the pinned Semgrep release accepts -the patched PyJWT versions. Validate native Semgrep scans after either change. +Semgrep 1.180.0 accepts `pyjwt[crypto]>=2.15.0,<3`, so the older security override +is retired. `tooling/semgrep/uv.lock` retains patched PyJWT 2.15.1 and its crypto +dependencies. Validate native Semgrep scans when changing the scanner or lock. +`just audit` includes both Python lockfiles, and Dependabot monitors both +environments. The isolated scanner remains on Python 3.14 while its wheels +exclude Python 3.15; support scripts run on Python 3.15. ### Numerical logging false positives diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md index eece397..6dab22e 100644 --- a/docs/BENCHMARKING.md +++ b/docs/BENCHMARKING.md @@ -33,7 +33,7 @@ next release; measured artifact links and historical reports retain their tags. |------|--------| | Latest-release local audit | `just performance-local` | | Non-exact release-signal check against tags | `just performance-local-non-exact "$CURRENT_TAG" "$PREVIOUS_TAG"` | -| Fast saved-baseline loop | `just bench-save-baseline ` then `just bench-compare all-benches` | +| Fast saved-baseline loop | `just bench-save-baseline ` then `just bench-compare ` | | Full crate comparison | `just bench-vs-linalg` | | Interval determinant filter | `just bench-interval` | | Certified dot/linear-form filter | `just bench-linear-form` | @@ -46,8 +46,7 @@ Rule of thumb: - Use `performance-local*` for clean, self-contained answers. - Use `bench-save-*` plus `bench-compare` for tight local optimization loops. -- Use `bench-vs-linalg` plus `plot-vs-linalg` for exploratory crate-to-crate - plots. +- Use `plot-vs-linalg` to render dimension plots from a retained complete run. - Use `performance-release` only when preparing committed release artifacts. - After `performance-release`, use `performance-readme` to publish its retained crate-to-crate measurements without benchmarking again. @@ -274,13 +273,13 @@ compare: ```bash just bench-vs-linalg-la-stack -just bench-compare inf-norm-before vs_linalg all-benches +just bench-compare inf-norm-before ``` -The `just bench-compare` recipe uses positional arguments: -`just bench-compare `. The underlying -`uv run --locked bench-compare` CLI accepts the explicit `--suite` and -`--scope` flags. +The `just bench-compare ` recipe calls the installed shared Criterion +comparison CLI. It reports every available row, additions and omissions; old +suite/scope flags are retired. These exploratory ratios do not validate a common +harness or scientific release eligibility. Use `performance-local` for that gate. `just bench-save-baseline ` writes Criterion samples under `target/criterion/`. `just bench-save-last` saves the conventional local @@ -310,7 +309,7 @@ just performance-readme `performance-readme` does not run benchmarks. It loads the canonical `target/bench-reports/performance.run.json` and its evidence envelope retained by -`performance-release`, falling back to the latest committed local snapshot when +`performance-release`, falling back to the validated retained complete-run history when all scratch inputs are absent. It then uses the current la-stack result and the peer nalgebra/faer results measured by that same shared current harness. It requires all three timings for every canonical dimension (D=2, 3, 4, 5, 8, 16, 32, and @@ -325,14 +324,20 @@ The publisher verifies the complete-run identity, release version, current source and harness inventories/digests, dependency resolution, complete peer coverage, and recorded phase provenance before writing. The derived sidecar records the run identity and the actual current/baseline origins of each series. -Historical CSV/JSON evidence retains its original reader and provenance. +Historical CSV/JSON evidence remains frozen with its original provenance. Missing, stale, or inconsistent input aborts publication atomically. +An existing local release tag must contain the exact selected asset bytes. +A future release tag may be absent during release preparation. -For exploratory CSV/SVG output, run `bench-vs-linalg` and `plot-vs-linalg`. -That path still reads raw Criterion output; `--allow-partial` remains -exploratory-only and cannot update README. +`plot-vs-linalg` renders the same complete-run selection without editing README. +The raw-Criterion/gnuplot renderer and CSV-to-README transport are retired after +complete-run integration parity. Historical figures and CSV/JSON remain unchanged; +they cannot be relabeled as complete runs. Raw exploratory timing reports remain +available through Criterion and `bench-compare`. Dimension publication requires +all canonical dimensions and uses the shared SVG, long-form CSV and table +renderers, with configured selections in `tooling/performance-plot.toml`. -See `uv run --locked criterion-dim-plot --help` for plotting options. +See `uv run --locked scripts/archive_performance.py plot --help` for plotting options. ### Create The Release Performance Report @@ -370,11 +375,16 @@ cases appear as an em dash. Each series identifies its measurement phase, release, and revision. Full environment, harness, command, and gate provenance remains in that run's `evidence.json`, beside `run.json` and `report.md`. New reports no longer use the custom percentage-change, ratio, or interval-overlap -columns. The README dimension plot and its table retain their existing format. +columns. README publication uses the shared coordinate SVG, long-form timing CSV +and per-series estimate table, retaining nanoseconds, intervals and original phases. Serialization, validation, rendering, and promotion failures preserve existing published reports and selection. Review and commit the whole new run and index. Legacy snapshots under `docs/performance/` retain their original bytes, -hashes, framing, and reader; see the [legacy index](performance/README.md). +hashes and framing; see the [legacy index](performance/README.md). +Their custom writers and report renderer are retired. Verified comparison-only +conversion uses `tooling/performance-legacy.toml` with the shared `performance convert` +command, retaining the original hashes and opaque metadata. It cannot reconstruct +raw samples or create complete-run provenance. The last published legacy report is preserved in the [Markdown archive](archive/performance/README.md). The checked-in current report keeps its historical format until a complete run is promoted; old summaries do @@ -399,8 +409,9 @@ just performance-doc This command fails closed on a missing, partial, malformed, mismatched, or unsupported artifact pair. It consumes the default complete-run payload/envelope or follows the validated `docs/performance-runs/latest.json` when all scratch -inputs are absent. A repository with only legacy evidence continues through -the historical reader. Report replay runs offline and updates the current +inputs are absent. Legacy-only input fails with a missing complete-run error; +run `performance-release` to create the supported replacement. Frozen historical +reports remain readable directly. Report replay runs offline and updates the current document and shared history transactionally. Promotion requires distinct current and baseline package versions, so a same-version local comparison is retained and reproducible but cannot become release documentation. Use promotion @@ -442,8 +453,8 @@ shared-harness workflow before attributing a difference solely to library code. | `target/bench-reports/github-assets-performance.md` | No | `performance-github-assets` | Local report from published release artifacts. | | `docs/performance.md` | Yes | `performance-release`, `performance-doc` | Current shared full report; last legacy report until complete-run promotion. | | `docs/performance-runs/` | Yes | `performance-release`, `performance-doc` | Immutable complete runs, reports, index, and validated latest pointer. | -| `docs/performance/` | Yes | Legacy reader | Unchanged historical CSV/JSON evidence and hashes. | -| `docs/archive/performance/` | Yes | Legacy reader | Preserved reports in the historical format. | +| `docs/performance/` | Yes | Frozen record/shared conversion | Unchanged historical CSV/JSON evidence and hashes. | +| `docs/archive/performance/` | Yes | Frozen record | Preserved reports in the historical format. | | `docs/archive/performance/studies/` | Yes | Maintainer investigations | Completed optimization studies and decisions. | | `docs/assets/bench/` | Yes | `performance-readme` | README benchmark CSV/SVG assets and JSON provenance. | | GitHub Release | Remote | `.github/workflows/release-benchmarks.yml` | Criterion baseline archive. | @@ -463,9 +474,10 @@ different machine; local summary comparisons must use their recorded environment ## `vs_linalg` Methodology `vs_linalg` is a per-kernel comparison, not a single aggregate score. Each row -compares one operation for one dimension `D`, using Criterion's selected -statistic from `target/criterion/d{D}/{benchmark}/{sample}/estimates.json`. -The README table uses `median.point_estimate` in nanoseconds. Lower is better, +compares one operation for one dimension `D`. Exploratory Criterion reports use +the selected statistic from `target/criterion/d{D}/{benchmark}/{sample}/estimates.json`. +The README table reads median estimates and intervals in nanoseconds from validated +complete-run evidence. Lower is better, but point-estimate ratios alone are descriptive and do not establish a statistically supported performance difference. Preserve Criterion confidence intervals or repeat controlled runs when making a stronger claim. @@ -755,11 +767,9 @@ baselines created before the `exact_random_corpus_d*` names were introduced do not have comparable full-corpus rows, so those rows appear once both sides of a comparison provide the stable group. -To generate a current snapshot without a saved baseline: - -```bash -uv run --locked bench-compare --snapshot -``` +For a current snapshot without a saved baseline, use Criterion’s native reports +under `target/criterion/`. The retired `bench-compare --snapshot` command is no +longer provided. ## Release Notes @@ -849,9 +859,10 @@ just markdown-ci just doc-check ``` -The Python suite executes the workflow's shell with simulated GitHub API -responses to test draft rejection, commit changes, upload failures, asset -verification, and safe reruns. Archive fixtures check the complete dataset. +The Python suite executes the workflow's shell with the pinned installed CLI +and a simulated GitHub provider to prove captured-target handoff, commit-change +rejection and explicit publication. Generic provider failure matrices belong +upstream. Archive fixtures retain scientific coverage and exact raw bytes. Every hosted dispatch now requires a real release draft and authorizes its publication; there is no producer-only manual mode. Use the diff --git a/docs/RELEASING.md b/docs/RELEASING.md index eab0c56..5f7b905 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -288,6 +288,13 @@ release ID and tag commit before attaching the archive and again before publication. Do not edit the draft, move the tag, or publish manually while the workflow is running. +The privileged jobs install the exact published `research-repo-tools==0.1.9` +package on Python 3.15 without repository checkout. Preflight emits versioned +target JSON with the captured ID, commit and title assertion. Its trusted job +output passes directly to the publisher, separately from the producer's inert +archive. The shared writer verifies downloaded asset bytes as well as provider +metadata and receives explicit `--publish` intent. + Runs for the same tag are serialized without cancelling an active run. ### 7. Verify publication and the durable Criterion baseline diff --git a/docs/code_organization.md b/docs/code_organization.md index b1740a7..d113aa4 100644 --- a/docs/code_organization.md +++ b/docs/code_organization.md @@ -155,17 +155,16 @@ upgrades and coupled exclusions against a disposable local registry. The shared package also owns process discovery, captured/live execution, exact byte transport, CPU metadata, diagnostics, and zizmor authentication. Consumers -call its public process API directly; `scripts/benchmark_contract.py` retains -Cargo checkout selection and historical digest framing. Shared Criterion -estimates replace local timing wrappers. Tests use shared Just inspection and -retain scientific schemas and consumer policy checks. +call its public process API directly. Shared Criterion estimates replace local +timing wrappers. Tests use shared Just inspection and retain scientific +selection and consumer policy checks. Performance consumers use shared Criterion parsing and estimate/comparison validation, digest verification, archive extraction, byte-preserving document sections, and multi-file transactions. Shared rendering produces complete-run -reports; local rendering remains for historical reports and dimension plots. -Historical artifact schemas and fingerprint framing, -benchmark selection and eligibility remain in the consumer. Published v0.1.8 +reports, coordinate SVG, timing CSV and document tables. Historical artifact +bytes and fingerprint framing remain frozen; declarative shared conversion reads +the original CSV/metadata without a legacy writer. Published v0.1.9 owns common-harness orchestration, completeness, worktrees, complete run retention, validated latest selection, and transactional report publication. Generic parsing, staging, and rollback tests belong upstream; local @@ -175,8 +174,10 @@ Hosted Dependabot approvals use the pinned shared GitHub workflow; the [rollout guide](dev/MANAGING_CHANGES.md#dependabot-approval-rollout) owns settings and deployment verification. -`scripts/release_baseline.py` owns release-suite inventory and delegates complete -raw Criterion validation to the shared policy. The release workflow packages only datasets that pass +`tooling/criterion-inventory.toml` declares native full-suite list commands and +minimum scientific report/plot coverage. `tooling/criterion-saved.toml` declares +complete saved-sample requirements. Shared discovery and saved-sample APIs own +inventory parsing and exact raw-file verification. The release workflow packages only datasets that pass that gate; its regression and archive tests live in `scripts/tests/test_release_baseline.py`. @@ -184,15 +185,26 @@ that gate; its regression and archive tests live in gates for both local measurements and release inventories. `tooling/performance.toml` uses the shared measurement schema for source/harness inputs, probes, timeouts, and compatibility fields. -`scripts/performance_runs.py` declares scientific coverage, -release compatibility, and named reference phases used by shared reports and local plots. -`scripts/archive_performance.py` composes the shared APIs for consumer commands. +`tooling/performance-policy.toml` owns the scientific metric catalog, exact groups, +peer mappings and historical API availability. `scripts/performance_runs.py` +binds those declarations to scientific coverage, release compatibility and +named reference-phase assertions. `scripts/archive_performance.py` composes +shared APIs for tracked-only measurements, metadata-free historical raw archives +and la-stack README publication. +`tooling/performance-plot.toml` declares dimensions, units, series and axes. `tooling/performance-report.toml` owns the canonical report path, title, and shared history path. -`scripts/benchmark_summaries.py` only reads historical CSV/JSON snapshots; -`scripts/criterion_measurements.py` and the old generic measurement/retention -engines are removed. `scripts/tests/test_performance_workflow.py` verifies -installed-package integration and offline replay. The retained local -`criterion_dim_plot.py` preserves the multi-library figure format. +`tooling/performance-legacy.toml` declares shared historical comparison conversion. +The old comparison renderer, CSV/provenance writer, summary reader, plotter, +release-inventory wrapper and Python packaging are removed. Only the two +consumer adapters remain; `scripts/tests/test_performance_workflow.py` verifies +installed-package integration and offline replay. Shared renderers preserve the +figure’s scientific coordinates, units and originating series. The +[adoption record](dev/performance-tooling-adoption.md) owns retained-helper +dispositions, Python 3.15 review and native validation gates. + +`tooling/semgrep/` owns a locked Python 3.14 environment for Semgrep 1.180.0 +while its wheels exclude Python 3.15. Local scans, shared fixture validation +and hosted SARIF use it; ordinary support scripts use Python 3.15. `.github/actions/prepare-release-benchmarks/action.yml` groups tool installation, input validation, and inventory under the release workflow's shared setup timeout. diff --git a/docs/dev/performance-tooling-adoption.md b/docs/dev/performance-tooling-adoption.md new file mode 100644 index 0000000..7ba4012 --- /dev/null +++ b/docs/dev/performance-tooling-adoption.md @@ -0,0 +1,114 @@ +# Performance tooling adoption + +This records [#276](https://github.com/acgetchell/la-stack/issues/276) against +the published `research-repo-tools==0.1.9` base package. No extras are required. +Publication prerequisite [upstream #99](https://github.com/acgetchell/research-repo-tools/issues/99) +is closed. PyPI's unyanked wheel and sdist hashes match the exact lockfile entries; +runtime code comes from the installed package. + +## Capability disposition + +| Upstream capability | Consumer adoption | Local responsibility | +| --- | --- | --- | +| #94 captured targets | Installed release-target CLI passes versioned JSON from trusted preflight to writer | Dispatch/ref equality, current attempt, title, privilege separation, asset filename and explicit publication | +| #95 coordinate plots | Shared coordinate SVG, table and CSV renderers | Scientific metric mappings, dimensions, nanoseconds and original phases | +| #96 provenance reports | Shared renderer 2 and publication-relative evidence links | Scientific eligibility and frozen historical records | +| #97 complete-run sections | Shared presentation and publication transaction | README markers, measured-asset pointers and current source/harness checks | +| #98 Criterion verification | Declarative shared discovery, versioned inventory and saved-sample APIs | Full Cargo suites/features, minimum scientific coverage and correctness gates | + +The full-report standardization from #277 remains. New reports expose recorded +provenance and evidence links. Shared mixed-renderer history preserves immutable +older complete-run reports while upgrading mutable indexes. + +## Python deletion audit + +The audit compared the dependency-only MCMC environment and declarative +`tooling/` commands with the installed v0.1.9 public APIs. + +| Former helper | Disposition and supported replacement | +| --- | --- | +| `bench_compare.py` | Deleted; saved-baseline reports use the installed shared Criterion comparison CLI | +| `benchmark_contract.py` | Deleted; metric/group/peer/API declarations moved to `tooling/performance-policy.toml`; historical digests stay frozen | +| `benchmark_summaries.py` | Deleted; frozen historical summaries are records rather than regeneration inputs | +| `criterion_dim_plot.py` | Deleted; shared complete-run SVG/CSV/table presentation replaces raw-Criterion/gnuplot rendering | +| `performance_artifacts.py` | Deleted; shared evidence/publication and declarative `performance-legacy.toml` replace CSV/provenance writers and readers | +| `release_baseline.py` | Deleted; native discovery uses the shared CLI and saved-copy verification uses the shared configured API | +| Local wheel/sdist and console-entry-point packaging | Removed; `uv` manages a dependency-only environment | +| Duplicate parser, renderer, legacy-writer and packaging tests | Removed; actual installed-package/configuration and scientific integration tests remain | + +Retirement conditions are satisfied by installed replacement checks, preservation +of original bytes/hashes and a documented supported workflow. The old report +layout need not be regenerated indefinitely. New reports use the shared format; +historical reports, CSV/JSON, indexes, plots and archives remain untouched. +Shared historical conversion verifies the original hash and retains opaque +metadata and original digest names. It produces comparison evidence, never +fabricated complete-run samples or newly captured host provenance. Retire its +conversion declaration only after verified companions exist for every original +and those original bytes remain independently available. + +Only two production modules remain: + +- `archive_performance.py`: la-stack release eligibility, tracked-only snapshots, + historical raw archives without metadata, and README publication composition. +- `performance_runs.py`: captured old-API cfg/environment, scientific selection, + sampling, originating reference phases and source/harness eligibility. + +These are consumer policies rather than shared implementations. v0.4.4/v0.4.5 +need the historical cfg and root-test layout adapter. Shared CLI snapshots include +untracked files; la-stack's established contract excludes them. The shared plan +cannot select cfg and API availability from a release pair. Raw historical release +archives lack the metadata required by the shared asset-evidence CLI. Their +small adapter uses shared parsing/rendering and explicitly identifies unrecorded +source, host and gates without constructing an evidence envelope. + +Generic subprocesses, discovery parsing, worktrees, measurement, completeness, +estimates, comparison arithmetic, rendering, run identity, history, downloads, +extraction and publication transactions all remain upstream. The retained Python +calls public APIs. Scientific names and mappings live in `tooling/`. + +## Python 3.15 review + +Metadata, `.python-version`, the locked environment and CI select Python 3.15. +The remaining code uses deferred annotations, typed boundaries and explicit +byte/UTF-8 transport. Scientific catalogs parse into immutable typed records before +discovery; malformed dimensions, names, peer pairs and metric rows retain field +diagnostics. Existing evidence records are supplied by the shared +package. Deleting the historical serializer also removes its custom recursive +freezing/thawing machinery. No artificial syntax modernization replaces a +supported shared implementation. + +Semgrep 1.180.0 wheels stop at Python 3.14. `tooling/semgrep/` pins the current +scanner and Python 3.14. The obsolete PyJWT override is retired because the pinned +scanner accepts patched PyJWT 2.15.1 with its crypto extra. Local scans, +fixture validation and hosted SARIF use that lock. Retire this exception after +current published wheels support Python 3.15 and native gates pass. + +## Size and validation evidence + +Counts use physical lines from `wc -l scripts/*.py scripts/tests/*.py` and +`uv run --locked pytest --collect-only -q`. Baseline is `905aba7`; test counts +describe consumer checks, not measurements. + +| Measure | Before | After | +| --- | ---: | ---: | +| Production modules | 8 | 2 | +| Production Python lines | 5,323 | 788 | +| Test Python lines | 5,096 | 1,952 | +| Collected tests | 371 | 130 | + +Production Python is reduced by about 85%. Focused installed-package tests cover +complete-run gates/coverage, original reference phases, offline report/plot replay, +source membership and unindexed-history freshness, captured release targets, +native archive round trips and unchanged historical hashes. The actual saved +comparison recipe and declarative historical conversion passed replacement checks. +Native discovery compiled/listed 533 comparison and 264 exact cases with all +417 required report/plot IDs, without collecting timings. + +Local executor: macOS arm64, CPython 3.15.0; Semgrep uses CPython 3.14.7. +The final repository validation includes Python format/lint/types, consumer tests, +Rust tests/doctests, docs, examples, benchmark compilation and native static checks. +No new scientific measurements or regenerated historical artifacts are claimed. + +Issue closure still requires an adoption commit, native Linux/macOS/Windows CI +for that exact commit, linked run evidence and merge before v0.5.0. Local macOS +validation does not substitute for native Linux or Windows results. diff --git a/justfile b/justfile index 4a1f757..d48c28b 100644 --- a/justfile +++ b/justfile @@ -62,19 +62,15 @@ action-lint: _ensure-actionlint # Audit the repository's Python and Rust lockfiles with the managed OSV scanner. audit: - uv run --locked --group dev research-repo-tools security osv uv.lock Cargo.lock + uv run --locked --group dev research-repo-tools security osv uv.lock tooling/semgrep/uv.lock Cargo.lock # Run the fixed-dimension benchmark suites. bench: {{ _run }} cargo bench --locked --workspace --features bench -# Compare latest measurements against a saved baseline. -# Defaults to the `last` full-release baseline. -bench-compare baseline="last" suite="all" scope="release-signal": python-sync - #!/usr/bin/env bash - set -euo pipefail - baseline={{ quote(baseline) }} - {{ _run }} uv run --locked bench-compare "$baseline" --suite {{ quote(suite) }} --scope {{ quote(scope) }} +# Compare every available Criterion row with a saved baseline; exploratory only. +bench-compare baseline="last": python-sync + {{ _run }} uv run --locked research-repo-tools performance compare target/criterion target/criterion --baseline-sample {{ quote(baseline) }} --format markdown --output target/bench-reports/performance.md # Compile benchmark targets with warnings denied; do not measure timings. bench-compile: @@ -94,8 +90,8 @@ bench-interval: bench-latest: bench-vs-linalg-la-stack bench-exact # Run latest measurements and render the latest-vs-last performance report. -bench-latest-vs-last baseline="last": bench-latest python-sync - {{ _run }} uv run --locked bench-compare {{ quote(baseline) }} +bench-latest-vs-last baseline="last": bench-latest + just bench-compare {{ quote(baseline) }} # Run the certified dot-product and affine-difference benchmark suite. bench-linear-form: @@ -103,11 +99,15 @@ bench-linear-form: # Check every discovered benchmark before the release workflow packages it. bench-release-check tag: _ensure-uv - {{ _run }} uv run --locked scripts/release_baseline.py validate --baseline {{ quote(tag) }} + {{ _run }} uv run --locked scripts/archive_performance.py release-check {{ quote(tag) }} # Discover all release benchmarks and report their expected measurement budget. bench-release-inventory: _ensure-uv - {{ _run }} uv run --locked scripts/release_baseline.py inventory + #!/usr/bin/env bash + set -euo pipefail + [[ ! -e "${CRITERION_HOME:-target/criterion}" ]] || { echo "Release discovery requires a fresh Criterion directory" >&2; exit 1; } + {{ _run }} uv run --locked research-repo-tools performance discover tooling/criterion-inventory.toml --output target/release-benchmark-inventory.json + jq -r '.suites | to_entries[] | "[release-baseline] \(.key): \(.value | length) benchmarks; nominal warmup + measurement \((.value | length) * 8 / 60) min; planning estimate at 12 s/benchmark \((.value | length) * 12 / 60) min (not an upper bound)"' target/release-benchmark-inventory.json # Save a Criterion baseline. Defaults to all release-signal benchmark suites. bench-save-baseline tag suite="all": @@ -150,9 +150,9 @@ bench-vs-linalg filter="": bench-vs-linalg-la-stack: {{ _run }} cargo bench --locked -p la-stack-comparison --features bench --bench vs_linalg -- la_stack --noplot -# Run only la-stack vs_linalg measurements and render a non-exact performance report. -bench-vs-linalg-latest-vs baseline="last": bench-vs-linalg-la-stack python-sync - {{ _run }} uv run --locked bench-compare {{ quote(baseline) }} --suite vs_linalg --scope release-signal +# Run only la-stack vs_linalg measurements, then compare available Criterion rows. +bench-vs-linalg-latest-vs baseline="last": bench-vs-linalg-la-stack + just bench-compare {{ quote(baseline) }} # Quick iteration (reduced runtime, no Criterion HTML). bench-vs-linalg-quick filter="": @@ -367,23 +367,23 @@ markdown-fix: tools-check echo "No markdown files found to format." fi -# Render release docs from complete runs, or read historical CSV/JSON evidence. +# Render release docs from complete runs or their validated retained history. performance-doc: python-sync - {{ _run }} uv run --locked archive-performance report + {{ _run }} uv run --locked scripts/archive_performance.py report # Compare published release archives with their original per-release harnesses. performance-github-assets current_tag="" baseline_tag="": _ensure-gh python-sync - {{ _run }} uv run --locked archive-performance assets {{ quote(current_tag) }} {{ quote(baseline_tag) }} + {{ _run }} uv run --locked scripts/archive_performance.py assets {{ quote(current_tag) }} {{ quote(baseline_tag) }} # Measure tracked current inputs and the latest release with the captured current harness. performance-local: _ensure-gh python-sync - {{ _run }} uv run --locked archive-performance local + {{ _run }} uv run --locked scripts/archive_performance.py local # Measure the comparison suite while retaining baseline-phase nalgebra/faer context. performance-local-non-exact current_tag="" baseline_tag="": _ensure-gh python-sync - {{ _run }} uv run --locked archive-performance local {{ quote(current_tag) }} {{ quote(baseline_tag) }} --suite vs_linalg --stem target/bench-reports/performance-non-exact + {{ _run }} uv run --locked scripts/archive_performance.py local {{ quote(current_tag) }} {{ quote(baseline_tag) }} --suite vs_linalg --stem target/bench-reports/performance-non-exact -# Publish the existing README plot format from retained measurements after cleanup. +# Publish the dimension plot and README section directly from complete-run evidence. performance-readme metric="lu_solve" stat="median" sample="new" log_y="true": python-sync #!/usr/bin/env bash set -euo pipefail @@ -391,24 +391,21 @@ performance-readme metric="lu_solve" stat="median" sample="new" log_y="true": py if [ {{ quote(log_y) }} = "true" ]; then args+=(--log-y) fi - {{ _run }} uv run --locked criterion-dim-plot "${args[@]}" + {{ _run }} uv run --locked scripts/archive_performance.py plot "${args[@]}" # Measure, retain an immutable complete run, and promote distinct-release documentation. performance-release current_tag="" baseline_tag="": _ensure-gh python-sync - {{ _run }} uv run --locked archive-performance release {{ quote(current_tag) }} {{ quote(baseline_tag) }} + {{ _run }} uv run --locked scripts/archive_performance.py release {{ quote(current_tag) }} {{ quote(baseline_tag) }} -# Plot: generate a single time-vs-dimension SVG from Criterion results. -plot-vs-linalg metric="lu_solve" stat="median" sample="new" log_y="false" allow_partial="false": python-sync +# Plot the complete retained comparison without measuring again. +plot-vs-linalg metric="lu_solve" stat="median" sample="new" log_y="false": python-sync #!/usr/bin/env bash set -euo pipefail args=(--metric {{ quote(metric) }} --stat {{ quote(stat) }} --sample {{ quote(sample) }}) if [ {{ quote(log_y) }} = "true" ]; then args+=(--log-y) fi - if [ {{ quote(allow_partial) }} = "true" ]; then - args+=(--allow-partial) - fi - {{ _run }} uv run --locked criterion-dim-plot "${args[@]}" + {{ _run }} uv run --locked scripts/archive_performance.py plot "${args[@]}" # Check Python formatting, lint, fixtures, and types. python-check: python-format-check python-lint python-fixture-lint python-typecheck @@ -467,11 +464,11 @@ security-secrets: # Repository-owned Semgrep rules for project-specific diagnostics. semgrep: _ensure-uv - uv run --locked semgrep --metrics off --error --strict --timeout 30 --exclude tests/semgrep/src/project_rules/algebraic_float.rs --config semgrep.yaml . + uv run --locked --project tooling/semgrep semgrep --metrics off --error --strict --timeout 30 --exclude tests/semgrep/src/project_rules/algebraic_float.rs --config semgrep.yaml . # Fixture tests for repository-owned Semgrep rules. semgrep-test: python-sync - uv run --locked --group dev research-repo-tools semgrep check-fixtures + uv run --locked --project tooling/semgrep uv run --locked --project . --group dev research-repo-tools semgrep check-fixtures # Install declared managed tools and explicitly synchronize dev. setup: _ensure-gh _ensure-jq diff --git a/pyproject.toml b/pyproject.toml index 9afa5cd..1d2aef6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,47 +1,9 @@ -[build-system] -requires = [ "setuptools>=84.0.0", "wheel" ] -build-backend = "setuptools.build_meta" - [project] name = "la-stack-scripts" version = "0.4.6" -description = "Python utility scripts for the la-stack Rust library" -readme = "scripts/README.md" -requires-python = ">=3.14" -license = "BSD-3-Clause" -authors = [ - { name = "Adam Getchell", email = "adam@adamgetchell.org" }, -] -keywords = [ "linear-algebra", "benchmarking", "utilities", "la-stack" ] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.14", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: System :: Benchmarking", -] - -# Benchmark adapters use only the installed shared package and the standard library. -dependencies = [ "research-repo-tools==0.1.8" ] - -[project.urls] -"Homepage" = "https://github.com/acgetchell/la-stack" -"Documentation" = "https://docs.rs/la-stack" -"Repository" = "https://github.com/acgetchell/la-stack" -"Bug Tracker" = "https://github.com/acgetchell/la-stack/issues" - -[project.scripts] -archive-performance = "archive_performance:main" -bench-compare = "bench_compare:main" -criterion-dim-plot = "criterion_dim_plot:main" - -# Configure setuptools to find modules in scripts/ directory. -[tool.setuptools] -package-dir = { "" = "scripts" } -py-modules = [ "archive_performance", "bench_compare", "benchmark_contract", "benchmark_summaries", "criterion_dim_plot", "performance_artifacts", "performance_runs", "release_baseline" ] +description = "Dependency-only development environment for la-stack" +requires-python = ">=3.15" +dependencies = [ ] [tool.ruff] line-length = 160 @@ -65,35 +27,22 @@ ignore = [ "TRY301", # Abstract raise to inner function - OK for straightforward CLI error handling "ARG001", # Unused function argument - common in callbacks "ERA001", # Found commented-out code - OK for explanatory comments - "EXE001", # Shebang present but file not executable - handled by packaging - "PTH123", # open() should be replaced by Path.open() - OK for some call sites + "EXE001", # Support adapters are invoked explicitly through uv "EM102", # Exception must not use f-string - OK for CLI error messages "TRY003", # Avoid specifying long messages outside exception class - OK for CLI reporting - "N806", # Uppercase local names are acceptable for constants and ANSI color codes - "F841", # Color constants may be assigned conditionally for CLI output - "PLW2901", # Loop-variable replacement is acceptable for line-processing transforms ] [tool.ruff.lint.per-file-ignores] "**/tests/test_*.py" = [ "S101", "SLF001", "D101", "D102", "D103" ] # Static-analysis fixtures intentionally contain the patterns these rules reject. -"tests/semgrep/scripts/python_portability.py" = [ "D103", "INP001", "N813", "PLW1510", "S603", "S607", "SIM115", "UP020" ] +"tests/semgrep/scripts/python_portability.py" = [ "D103", "INP001", "N813", "PLW1510", "PTH123", "S603", "S607", "SIM115", "UP020" ] "tests/semgrep/scripts/tests/python_exceptions.py" = [ "BLE001", "D100", "D103", "EM101", "INP001", "S110", "S607", "SIM105" ] [tool.ruff.lint.mccabe] max-complexity = 10 [tool.ruff.lint.isort] -known-first-party = [ - "archive_performance", - "bench_compare", - "benchmark_contract", - "benchmark_summaries", - "criterion_dim_plot", - "performance_artifacts", - "performance_runs", - "release_baseline", -] +known-first-party = [ "archive_performance", "performance_runs" ] force-single-line = false split-on-trailing-comma = true combine-as-imports = true @@ -116,6 +65,7 @@ line-ending = "auto" minversion = "9.0" addopts = [ "-ra", "--strict-markers", "--strict-config", "--color=yes" ] testpaths = [ "scripts/tests" ] +pythonpath = [ "scripts" ] python_files = [ "test_*.py", "*_test.py" ] python_classes = [ "Test*" ] python_functions = [ "test_*" ] @@ -137,25 +87,20 @@ disable = [ line-length = 160 [tool.uv] -package = true -required-version = "==0.13.0" -# Semgrep wheels bundle the native engine; building its sdist does not. -no-build-package = [ "semgrep" ] -# Semgrep 1.178.0 restricts PyJWT to 2.13.x; 2.15.1 contains the OSV fixes. -# Keep crypto support while overriding that restriction until upstream updates it. -override-dependencies = [ "pyjwt[crypto]>=2.15.1,<3" ] +package = false +required-version = "==0.13.1" +# Semgrep has its own locked Python 3.14 environment under tooling/semgrep. [dependency-groups] -tooling = [ "research-repo-tools==0.1.8" ] +tooling = [ "research-repo-tools==0.1.9" ] dev = [ { include-group = "tooling" }, "actionlint-py==1.7.12.25", "pytest==9.1.1", "ruff==0.17.0", - "semgrep==1.180.0", "shellcheck-py==0.11.0.1", "shfmt-py==4.2.0", - "ty==0.0.86", + "ty==0.0.87", "yamllint==1.38.0", ] @@ -164,7 +109,7 @@ schema = 1 [tool.research-repo-tools.toolchain.binaries] gitleaks = "8.30.1" -osv-scanner = "2.6.0" +osv-scanner = "2.7.0" [tool.research-repo-tools.toolchain.cargo] cargo-edit = "0.13.13" @@ -172,7 +117,7 @@ cargo-llvm-cov = "0.9.1" cargo-machete = "0.9.2" cargo-nextest = "0.9.148" clippy-sarif = "0.8.0" -dprint = "0.61.1" +dprint = "0.63.0" git-cliff = "2.14.2" rumdl = "0.2.79" sarif-fmt = "0.8.0" diff --git a/scripts/README.md b/scripts/README.md index 8c30e34..45af1e0 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,456 +1,102 @@ # scripts/ -This directory contains Python utilities used during development of the `la-stack` crate. +Only two production Python adapters remain. Generic tools run from the installed +`research-repo-tools==0.1.9` base package; their declarations belong in +`tooling/` and `pyproject.toml`. The project is a dependency-only environment, +with no local Python distribution, build backend or console entry points. ## Setup -The Python 3.14 support tooling in this repo is managed with -[`uv`](https://github.com/astral-sh/uv) and resolved from `uv.lock`. - -```bash -just python-sync -# or: -uv sync --locked --group dev -``` - -## Python maintenance rules - -- Keep Python 3.14 code precisely typed and add focused pytest coverage for - changed behavior and error paths. -- Mock subprocess results as `subprocess.CompletedProcess[str]`, matching the - production boundary. -- Catch only specific, recoverable exceptions; do not use broad - `except Exception` handlers. -- Give writer/parser pairs round-trip tests and explicit malformed-input tests. -- Update this README whenever Python entry points in `pyproject.toml` change. - -### Updating dependencies and repository-owned tools - -Run `just update` for the deliberate shared maintenance workflow. Tool updates -run first: uv upgrades through its installation owner, declared managed Cargo -tools upgrade with verified TOML pins, and setup synchronizes the environment. -Just follows the shared package's `rust-just` dependency. Dependency updates then -advance Cargo requirements and exact direct Python `dev` pins, refresh both -locks, and explicitly synchronize `dev` with managed Rust available. A failed -step stops subsequent work without rolling back earlier package-manager steps. -The coupled `num-bigint` and `num-rational` requirements are excluded from -independent incompatible upgrades and must be advanced together. The Python -shared updater asks uv to resolve one cross-platform tool set before applying all -changed exact pins together; it does not change runtime or build-system -requirements. - -## How to use it - -### Comparing performance - -The comparison script reads Criterion output and writes a local report to -`target/bench-reports/performance.md` by default: - -```bash -# Re-render from existing Criterion output -just bench-compare -``` - -Use `uv run --locked bench-compare --snapshot` for a no-baseline snapshot, or -`uv run --locked bench-compare ` to compare against a named saved -baseline. - -Use the top-level `just` workflows for routine release and local comparisons: - -`performance-github-assets` always requires the GitHub CLI (`gh`) authenticated -for the repository because it downloads release assets. Local-generation -recipes require `gh` only when discovering published release tags. - -New release comparisons require v0.4.4 or newer on both sides. v0.4.3 and older -are excluded; historical Markdown reports remain available in the archive. - -```bash -# Local development: compare the current tree with the latest release -just performance-local - -# Release PR: publish docs/performance.md and retain the complete run -just performance-release - -# Build release docs from retained complete-run evidence (or historical inputs) -just performance-doc - -# GitHub Actions release assets, without local cargo benchmark runs -just performance-github-assets -``` - -Local benchmark generation streams native Just, Cargo, and Criterion progress -while retaining the existing fail-closed report and provenance checks. Staged and -unstaged changes to tracked files participate. Untracked files are excluded; -stage a new file before running the command if it must participate. A local -current-vs-latest report may use the same package and release identifier because -its commit/ref and source-state provenance still distinguish the revisions. - -The local release workflows run the independent benchmark-input correctness gate -and then measure both library revisions with one hashed current benchmark -harness. Retained evidence records source-state, environment, toolchain, dependency, -Criterion, harness, and validation provenance and fails on incomplete selected -coverage. `performance-local` writes `performance.md`, `performance.run.json`, -and `performance.evidence.json` under `target/bench-reports/`. The shared -complete-run payload preserves every semantic Criterion case, 100 raw samples, -mean and median estimates, and 95% intervals from both phases. -`performance-release` requires distinct releases and publishes immutable -`run.json`, `evidence.json`, and `report.md` files under -`docs/performance-runs/runs//`, with a validated index and latest -pointer. The shared full report is published to `docs/performance.md` in the -same transaction, using the path and title in `tooling/performance-report.toml`. -Its mean and median tables show named series, marginal intervals, and unavailable -measurements, with each series attributed to its original phase. -Full provenance remains in the evidence envelope. Repeated runs for one pair coexist; publication failures -preserve the previous reports and selection. `performance-doc` replays this -evidence without Cargo. Same-version local comparisons require distinct source -identities and cannot be promoted as release reports. - -After `target/` cleanup, report and README commands select validated shared -history. Any partial scratch bundle fails instead of selecting older evidence. -Legacy CSV/JSON bytes, hashes, and rendered reports remain readable through -their original schema under `docs/performance/`; new runs never overwrite or -reinterpret that history. Native Criterion release archives remain the -durable raw baselines from the GitHub runner. Direct comparisons -of separately published artifacts retain their original per-release harnesses -and label unavailable historical measurement metadata explicitly. - -The comparison suite lives in the unpublished `benches/comparison` package, so -exact benchmark builds do not compile nalgebra/faer. Local release comparisons -measure la-stack on each revision and reuse peer measurements from the baseline -phase. The filtered current run skips Criterion HTML generation to avoid reading -missing `new` samples for peers that were not rerun. - -Operationally, `performance-release` is the atomic composition of -`performance-local` and `performance-doc`: measure, retain the common -comparison inputs, render, and promote. The narrowed -`performance-local-non-exact` view uses the same metrics and renderer with -nalgebra/faer context, but writes `performance-non-exact.*` so it does not -overwrite the canonical full comparison bundle. - -See `docs/BENCHMARKING.md` for the current command matrix, local saved-baseline -workflow, explicit tag arguments, output locations, and release-artifact -comparison details. - -### Plotting la-stack vs nalgebra/faer benchmarks - -The exploratory plotter reads Criterion output under: - -- `target/criterion/d{D}/{benchmark}/{new|base}/estimates.json` - -And writes: - -- `docs/assets/bench/vs_linalg_{metric}_{stat}.csv` -- `docs/assets/bench/vs_linalg_{metric}_{stat}.svg` -- `docs/assets/bench/vs_linalg_{metric}_{stat}.provenance.json` - -To generate the single “time vs dimension” chart: - -By default, the benchmark suite runs for dimensions 2–5, 8, 16, 32, and 64. - -1. For exploratory plots, run the benchmarks you want to plot (this produces - `target/criterion/...`): - -```bash -# full run (takes longer, better for exploratory plots) -just bench-vs-linalg lu_solve - -# or quick run (fast sanity check; still produces estimates.json) -just bench-vs-linalg-quick lu_solve -``` - -2. Generate an exploratory chart (median or mean): - -```bash -# median (recommended) -just plot-vs-linalg lu_solve median new true - -# or mean -just plot-vs-linalg lu_solve mean new true -``` - -For release publication, first retain the validated release comparison, then -update README's benchmark table (between `BENCH_TABLE` markers): +Use `just setup`, then `uv run --locked` for every Python command. The support +environment targets Python 3.15. `just python-check` checks formatting, lint and +types; `just test-python` runs focused installed-package consumer tests. + +Semgrep 1.180.0 has a separate locked Python 3.14 environment under +`tooling/semgrep/`, because its published wheels do not support Python 3.15. +Both local scans and hosted SARIF use that environment. Review its manifest and +refresh its lock deliberately when updating the scanner. + +## Performance tooling + +The [benchmark guide](../docs/BENCHMARKING.md) owns command selection, scientific +interpretation, output paths and release procedures. + +| Owner | Responsibility | +| --- | --- | +| `archive_performance.py` | Release eligibility, tracked-only snapshots, historical raw-archive composition, and la-stack README markers/pointers | +| `performance_runs.py` | Historical API cfg/environment, scientific coverage, sampling and original reference-phase assertions | +| `tooling/performance-policy.toml` | Exact group selections, metric names, peers, dimensions and historical API availability | +| `tooling/performance.toml` | Shared sources, harness, probes, dependencies, timeouts and compatibility | +| `tooling/performance.just` | Native Cargo discovery, independent correctness gates and timing commands | +| `tooling/performance-report.toml` | Shared current report and immutable complete-run history | +| `tooling/performance-plot.toml` | Scientific coordinate, unit, series and axis declarations | +| `tooling/criterion-inventory.toml` | Shared native discovery and minimum coverage of all report/plot consumers | +| `tooling/criterion-saved.toml` | Shared complete saved-sample policy, including exact `tukey.json` bytes | + +The adapters call public shared APIs for subprocesses, worktrees, Criterion +discovery, completeness, estimates, comparison arithmetic, evidence, rendering, +retention, publication, archive extraction and authenticated downloads. They +contain no generic renderer, CSV parser, legacy writer or measurement engine. + +The remaining composition is required by la-stack's contracts: versions before +v0.4.4 are ineligible; v0.4.4/v0.4.5 need the captured old-API cfg and root-test +layout adapter; local measurements exclude untracked files; nalgebra/faer +reference views retain their baseline origin. The shared CLI's default worktree +snapshot includes untracked files and its declarations cannot select an adapter +from a release pair. Historical release archives lack the metadata required by +the shared asset-evidence CLI, so their small adapter renders descriptive +comparisons without inventing source/host/gate provenance. + +`bench-compare` now calls the shared Criterion CLI directly. It compares every +available row, including additions and omissions, as an exploratory local report. +Its former suite/scope flags and snapshot CLI are retired. Use Criterion's native +reports for a single snapshot and `performance-local` for checked scientific +release comparisons. + +`performance-local` and `performance-release` retain complete native measurements. +`performance-doc` renders those inputs or validated shared history offline. +`performance-readme` and `plot-vs-linalg` use the shared complete-run SVG, CSV and +table renderers. The former preserves current source/harness membership and +digests and advances the three existing measured-artifact pointers transactionally. ```bash just performance-release just performance-readme lu_solve median new true +just plot-vs-linalg dot mean new true ``` -The README recipe consumes the complete-run payload and evidence envelope, -or the validated latest committed run after cleanup; -it does not run the benchmark-input gate or Criterion -again. It uses the current la-stack timing and retained same-current-harness -nalgebra/faer timings, requires all three at every canonical dimension, and then -publishes CSV, SVG, derived JSON provenance, the README table, and its tag-pinned -benchmark artifact links together. Until publication succeeds, those links keep -referencing the previous published artifacts. Partial dimensions are available -only through the raw-Criterion plotter's explicit `--allow-partial` exploratory -option and cannot update README. - -This writes: - -- `docs/assets/bench/vs_linalg_lu_solve_median.csv` -- `docs/assets/bench/vs_linalg_lu_solve_median.svg` (requires `gnuplot`) -- `docs/assets/bench/vs_linalg_lu_solve_median.provenance.json` -- the benchmark table and tag-pinned asset links in `README.md` - -(For `stat=mean`, the filenames end in `_mean` instead of `_median`.) - -### More examples - -Plot a different metric: - -```bash -uv run --locked criterion-dim-plot --metric dot --stat median --sample new -uv run --locked criterion-dim-plot --metric inf_norm --stat median --sample new -``` - -Plot a different statistic: - -```bash -uv run --locked criterion-dim-plot --metric lu_solve --stat mean --sample new -``` - -Plot the previous (baseline) sample instead of the newest run: - -```bash -uv run --locked criterion-dim-plot --metric lu_solve --stat median --sample base -``` - -Use a log-scale y-axis: - -```bash -uv run --locked criterion-dim-plot --metric lu_solve --stat median --sample new --log-y -``` - -Write to custom output paths: - -```bash -uv run --locked criterion-dim-plot \ - --metric lu_solve --stat median --sample new \ - --csv docs/assets/bench/custom.csv \ - --out docs/assets/bench/custom.svg -``` - -CSV only (skip SVG/gnuplot): - -```bash -uv run --locked criterion-dim-plot --no-plot --metric lu_solve --stat median --sample new -``` - -### gnuplot - -SVG rendering requires `gnuplot` to be installed and available on `PATH`. - -Install (macOS/Homebrew): +The raw-Criterion/gnuplot publisher, historical summary writer and custom +report renderer are retired after installed-package replacement checks. +Original CSV/JSON, reports, fingerprints and archives remain unchanged. +They are read directly as frozen records; no regeneration requirement keeps +their writers alive. For verified historical conversion into shared comparison +evidence, use `research-repo-tools performance convert` with +`tooling/performance-legacy.toml`; this never reconstructs complete runs from +summaries. Do not promote converted historical summaries as new measurements. +The conversion declaration remains until every retained original has a verified +shared comparison companion and its original bytes remain independently available. -```bash -brew install gnuplot -``` - -Verify the installed version: - -```bash -gnuplot --version -``` - -This repo has been tested with `gnuplot 6.0 patchlevel 3` (Homebrew `gnuplot 6.0.3`). +Generic parser, renderer, transaction and provider failure matrices belong +upstream. Consumer tests retain scientific selection/phase/gate checks, actual +native recipe wiring, complete release archive round trips, captured-target +handoff, offline publication and original artifact hashes. ## Changelog and release tooling -### Updating release metadata - -```bash -just update-version vX.Y.Z -``` - -The shared `research-repo-tools release update` CLI infers the previous stable -release from published GitHub releases and updates package, lockfile, citation, -and release metadata transactionally. Active README navigation remains on `main`; -measured artifact links retain their recorded revision. It records the current UTC -date in `CITATION.cff`. Pass `--previous-release vA.B.C` to avoid release discovery, -`--date YYYY-MM-DD` to declare a date, or `--dry-run` to preview validated changes: - -```bash -just update-version vX.Y.Z --previous-release vA.B.C --date YYYY-MM-DD --dry-run -``` - -Consumer policy in `pyproject.toml` checks the concept DOI and the expected README -reference count. Measured benchmark links, retained performance reports, archived -documentation, and dependency versions stay unchanged. Benchmark command examples -use selected tag variables rather than mutable release literals. If the target -changelog heading already exists, its date advances with the citation date. -After metadata preparation, `just docs-version-check` requires generated notes -for the new version; generate them with the same declared date before validation. - -### Generating the changelog - -```bash -# Full regeneration from all history -just changelog - -# Preview without replacing the root or archive files -just changelog-preview - -# Generate a prospective release with an explicit ISO date -just changelog-unreleased vX.Y.Z YYYY-MM-DD -# Equivalent name: -just changelog-release vX.Y.Z YYYY-MM-DD - -# Rotate existing notes without regenerating Git history -just changelog-archive -just changelog-check -just release-notes vX.Y.Z -``` - -The exact published `research-repo-tools==0.1.8` dependency is in `tooling`, -included by `dev`, and resolved from PyPI in `uv.lock`. Thin Just recipes use -the documented `research-repo-tools changelog` CLI. Its implementation and -common regressions belong to the shared package; this repository owns the -configuration, recipes, and `scripts/tests/test_changelog_integration.py`. -Internal shared modules are not a supported consumer API. - -The shared `deps update-python` command advances only exact requirements -declared directly in `dev`. Included groups retain their own upgrade policy; -the shared `tooling` pin and its lockfile change together through an intentional -dependency upgrade. - -`just changelog` generates, normalizes, formats, and rotates completed minor -series in one operation. It retains Unreleased and the newest minor series in -`CHANGELOG.md`, with older releases in `docs/archives/changelog/MAJOR.MINOR.md`. -Existing dated headings remain authoritative. Prospective generation requires -a `v`-prefixed SemVer tag and an explicit ISO date, and leaves package and -citation metadata untouched. Prepare metadata separately with -`just update-version`; choose the same date recorded in `CITATION.cff`. -Preview validates root and archive candidates without publishing them. -Malformed versions/dates, duplicate releases, conflicting retained notes, -and formatter failures stop publication with diagnostics. - -The shared changelog table in `pyproject.toml` declares the repository owner, -repository name, formatter, and `dependency-bodies = "preserve"`. The installed -v0.1.8 common template owns grouping, tag grammar, links, and Markdown handling; -there is no local `cliff.toml` or template exception. Installed-package tests -exercise Ruff, Ty, and setuptools authored text, links, and fenced code across -repeat generation, and check retained historical notes. -The shared normalizer preserves complete breaking-change descriptions, Markdown -links, and literal code in the input while adding merged-PR summaries. -It retains embedded conventional headings that the old normalizer deduplicated. - -`changelog-rumdl.toml` inherits the repository Markdown policy and disables -MD057 only while formatting unpublished candidates, whose future archive paths -do not yet exist. Repository Markdown checks and focused archive-link checks -still validate published destinations. The archive move preserves all release -dates and links; generated whitespace changes align existing series with the -shared formatter so later generation remains conflict-free. - -Setup, tool verification, managed execution, and updates now use the published -shared CLI. Consumer declarations stay in `.python-version`, `rust-toolchain.toml`, -and `pyproject.toml`; update recipes retain coupled Cargo exclusions and explicit -dev synchronization. `tests/test_toolchain_integration.py` exercises the actual -recipes with recording update boundaries and verifies native managed execution. -The shared CLI also owns release metadata and version checks, Markdown line -checks, and Semgrep fixture validation. `tests/test_maintenance_integration.py` -checks the actual release selectors, preserved scientific evidence, DOI policy, -and recipe forwarding. `tests/test_cargo_update_integration.py` executes native -Cargo upgrades against a disposable local registry; Python updates have a -matching real-uv fixture in the toolchain tests. Common parser, transaction, -Markdown, and fixture regressions belong to the shared package. Scientific -eligibility, Cargo commands/features, release adapters, selected rows, historical -report layouts, and dimension plots remain consumer-owned. The shared package -owns complete-run reports, common-harness phases, completeness, worktrees, run -identities, retention, and publication. -Notebook tooling remains outside scope. - -The same pinned release owns opt-in CodeRabbit review orchestration through -`research-repo-tools review branch --base=REF` and `review uncommitted`. -Thin Just wrappers retain the common implementation upstream; consumer checks -in `tests/test_review_integration.py` exercise recipe forwarding, argument -quoting, and failure propagation with local stubs. -CodeRabbit remains externally installed and authenticated. See the -[contributor review workflow](../CONTRIBUTING.md#coderabbit-review) for scopes -and invocation policy. - -### Creating a release tag - -```bash -just tag vX.Y.Z # create an annotated tag matching Cargo.toml -just tag-force vX.Y.Z # replace that tag only when explicitly repairing it -``` - -These recipes call `research-repo-tools changelog tag`. It searches the root -and canonical archives, validates the entire history, and requires the -`v`-prefixed tag to match the Cargo package version. The consumer's `declared` -date policy requires the dated release heading to match any `CITATION.cff` date, -so a release prepared before merge can be tagged on a later UTC day. Oversized annotations -link to the full notes to respect GitHub's 125KB limit. `--force` replaces an -existing local ref only after validation; it does not first delete the old tag. -Tagging never pushes or publishes a release. Use the CLI's `--dry-run` to -preview an annotation without creating a tag. - -### Scripts overview - -| Script | Purpose | -|---|---| -| `archive_performance.py` | Select eligible releases and compose shared measurement/publication APIs | -| `bench_compare.py` | Compare Criterion benchmark baselines and render Markdown reports | -| `benchmark_contract.py` | Select the Cargo checkout and hash the historical benchmark inventory contract | -| `benchmark_summaries.py` | Read historical complete CSV/JSON summaries without changing framing | -| `criterion_dim_plot.py` | Plot Criterion benchmark results (CSV + SVG + README table) | -| `performance_runs.py` | Declare scientific run policy and adapt retained series for dimension plots | -| `performance_artifacts.py` | Validate and publish schema-versioned performance-comparison CSV/JSON inputs | -| `release_baseline.py` | Inventory full Criterion suites and validate complete raw release baselines before packaging | - -Shared process discovery, execution, byte transport, CPU detection, diagnostics, -and zizmor authentication belong to research-repo-tools. Callers use its public -process API directly. Shared Criterion estimates and comparisons also replace -local timing wrappers and numerical validators. Consumer tests use the shared -Just inspection API and retain benchmark contracts, caller file-policy coverage, -and recipe forwarding; common parser and review regressions belong upstream. - -The `tooling/` directory owns declarative performance configuration: - -- `performance.toml`: shared measurement inputs, harness files, tool/dependency - probes, timeout, and provenance compatibility. -- `performance.just`: native Cargo inventory, independent gates, and timing - recipes, including baseline-only peer measurements. -- `performance-report.toml`: shared report and immutable-history paths. - -Both local measurement and release inventory use the native recipes. The shared -runner streams them in the measured checkout, with the recipes and measurement -configuration included in the harness fingerprint. Scientific row selection, -sampling requirements, historical API adapters, and figure layout remain Python. -Old evidence naming the retired Python phase driver can still be validated and -rendered offline; its recorded commands are never executed. - -The superseded `criterion_measurements.py`, archive/worktree measurement engine, -summary retention writer, and their duplicate generic tests are removed. -`test_performance_workflow.py` exercises the installed package through consumer -policies, failure preservation, historical golden bytes, and offline replay. -The shared multi-series plotting extension is deferred, so the existing local -CSV/SVG/README adapter preserves figure format and baseline-phase peer labels. -Complete-run Markdown uses the shared renderer directly. Upstream follow-ups -cover [coordinate plots](https://github.com/acgetchell/research-repo-tools/issues/95), -[provenance summaries](https://github.com/acgetchell/research-repo-tools/issues/96), and -[complete-run document publication](https://github.com/acgetchell/research-repo-tools/issues/97). +Use the shared commands exposed by `just --list`. The [release guide](../docs/RELEASING.md) +owns release metadata, changelog generation, tagging and registry publication. +Repository declarations in `pyproject.toml` retain the concept DOI, active link +policy, declared date policy, changelog formatter and dependency-body handling. -Performance scripts also use the published shared Criterion parser and estimate -validation, comparison arithmetic, exact-byte digest verification, safe archive -extraction, document marker replacement, and multi-file publication transaction. -Raw Criterion numeric strings are rejected by the shared parser. Plotting and -release publication still require complete confidence intervals; hosted baselines -and full local summaries additionally require 100 samples and 95% intervals. -README marker replacement preserves bytes outside the selected section. -Publication candidates are fully rendered and validated before one transaction -replaces the report, evidence, archive index, and retained summaries. Shared -recovery errors identify preserved backups if rollback fails. +The exact shared package pin lives in the `tooling` dependency group, included +by `dev`, and is resolved from PyPI in `uv.lock`. No sibling checkout supplies +runtime code. Shared `deps update-python` advances direct exact development +requirements; the shared package pin advances deliberately with its lockfile. -The remaining modules own la-stack's historical CSV/JSON schemas and fingerprints, -scientific eligibility, benchmark inventories, Cargo commands, release adapters, -release selection policy, and multi-library plot layout. -Existing evidence keeps its schema and digest framing. Generic parser and -transaction regressions belong upstream; consumer tests check retained-schema -round trips, complete output groups, failure preservation, and scientific policy. -The published v0.1.8 contract owns common-harness installation, independent -gates, measurement freshness, configurable completeness, and immutable retention. +Setup, tool verification, managed execution, dependency updates, Markdown line +checks, Semgrep fixture validation, security scanning and opt-in CodeRabbit +review also use shared commands. Consumer tests check configuration and actual +recipe forwarding; shared implementation regressions belong upstream. -See `docs/RELEASING.md` for the full release workflow. +See the [adoption record](../docs/dev/performance-tooling-adoption.md) for deletion +dispositions, Python 3.15 review, size measurements and validation evidence. diff --git a/scripts/archive_performance.py b/scripts/archive_performance.py index 23e7533..cae26b7 100644 --- a/scripts/archive_performance.py +++ b/scripts/archive_performance.py @@ -3,51 +3,45 @@ import argparse import json +import os import re import shutil import subprocess import sys import tempfile import tomllib -from dataclasses import dataclass, replace +from dataclasses import replace from pathlib import Path +from typing import Literal from research_repo_tools.archives import extract_archive from research_repo_tools.common_measurement import measure_prepared_pair from research_repo_tools.complete_runs import render_run, run_identity from research_repo_tools.evidence import Evidence, serialize_evidence -from research_repo_tools.files import replace_many from research_repo_tools.measurement import resolve_revision from research_repo_tools.process import ExecutableNotFoundError, format_exception_diagnostics, run_command -from research_repo_tools.publication import plan_outputs, publish_publication +from research_repo_tools.publication import GitCheck, GlobCheck, MarkerPair, RunSelectionCheck, plan_outputs, publish_publication, replace_section from research_repo_tools.release_assets import download_release_asset from research_repo_tools.release_discovery import normalize_tag, stable_published_releases from research_repo_tools.release_pairs import PairMode, ReleasePair, resolve_pair -from research_repo_tools.run_reports import load_run_report_plan +from research_repo_tools.run_presentation import CoordinatePlot, RunLayout, RunRow, SeriesSelection, render_coordinate_svg, render_run_table, serialize_run_csv +from research_repo_tools.run_reports import load_latest_run, load_run_report_plan from research_repo_tools.worktrees import apply_snapshot, capture_snapshot, temporary_worktree -from bench_compare import render_historical_assets, render_release_artifacts -from benchmark_contract import find_project_root -from benchmark_summaries import resolve_report_paths -from performance_artifacts import ArtifactPaths, load_bundle, resolve_shared_harness_compatibility from performance_runs import ( REPORT_CONFIG, archive_directory, + compatibility_adapter, + find_project_root, measurement_plan, + required_report_ids, retained_run, + scientific_policy, scratch_paths, validate_scientific_run, ) -@dataclass(frozen=True) -class ReportId: - """Legacy release labels, retained without changing historical artifact framing.""" - - current_tag: str - baseline_tag: str - - def select_pair(root: Path, mode: PairMode, current: str | None, baseline: str | None, *, local: bool = False) -> ReleasePair: """Use publication chronology while retaining la-stack release eligibility.""" package = tomllib.loads((root / "Cargo.toml").read_text(encoding="utf-8"))["package"]["version"] @@ -65,7 +59,7 @@ def select_pair(root: Path, mode: PairMode, current: str | None, baseline: str | pair = ReleasePair(current, baseline) else: pair = resolve_pair(mode, package_tag=package, releases=releases, current=current, baseline=baseline, order="published") - resolve_shared_harness_compatibility(current=pair.current, baseline=pair.baseline, shared_harness_rational_inputs=True) + compatibility_adapter(pair, root) return pair @@ -89,7 +83,7 @@ def measure(root: Path, pair: ReleasePair, suite: str, scope: str) -> Evidence: # Consumer API-layout adapter: the comparison test moved to its own # workspace member and the historical root copy must not compile. (baseline / "tests/vs_linalg_inputs.rs").unlink(missing_ok=True) - plan = measurement_plan(current, pair, suite, scope, parent / "listing") + plan = measurement_plan(current, pair, suite, scope) evidence = measure_prepared_pair(baseline, current, plan, pair, working_tree=True) if replace(capture_snapshot(root), untracked=()) != snapshot: msg = "source working tree changed while measurement ran" @@ -98,16 +92,21 @@ def measure(root: Path, pair: ReleasePair, suite: str, scope: str) -> Evidence: # A failed shared worktree cleanup leaves its path intact for recovery. if not (parent / "baseline").exists() and not (parent / "current").exists(): shutil.rmtree(parent) - validate_scientific_run(evidence) + validate_scientific_run(evidence, root=root) return evidence def save_scratch(root: Path, stem: Path, evidence: Evidence) -> None: """Save complete evidence and the shared report without touching legacy files.""" - validate_scientific_run(evidence) + validate_scientific_run(evidence, root=root) payload, manifest = scratch_paths(stem) data, envelope = serialize_evidence(evidence) - report = render_run(evidence) + report = render_run( + evidence, + report_path=stem.with_suffix(".md").relative_to(root).as_posix(), + payload_path=payload.relative_to(root).as_posix(), + manifest_path=manifest.relative_to(root).as_posix(), + ) plan = plan_outputs( root, {path.relative_to(root).as_posix(): value for path, value in ((payload, data), (manifest, envelope), (stem.with_suffix(".md"), report))}, @@ -119,7 +118,7 @@ def save_scratch(root: Path, stem: Path, evidence: Evidence) -> None: def promote(root: Path, stem: Path) -> None: """Validate consumer policy and publish the shared report and history plan.""" evidence = retained_run(root, stem) - validate_scientific_run(evidence, release=True) + validate_scientific_run(evidence, release=True, root=root) payload, manifest = scratch_paths(stem) kwargs = {} if payload.exists() or manifest.exists(): @@ -132,87 +131,78 @@ def promote(root: Path, stem: Path) -> None: publish_publication(history) -def parse_report_id(text: str) -> ReportId: - """Read the established scientific report header without modifying its bytes.""" - current = re.findall(r"^\*\*la-stack\*\* (v[^\s]+)", text, re.MULTILINE) - baseline = re.findall(r"^Comparison against baseline \*\*([^*]+)\*\*:", text, re.MULTILINE) - if len(current) != 1 or len(baseline) != 1: - msg = "performance report must contain exactly one current and baseline release identity" - raise ValueError(msg) - return ReportId(normalize_tag(current[0]), normalize_tag(baseline[0])) - +def compare_assets(root: Path, pair: ReleasePair, output: Path) -> None: + """Compare raw historical archives without inventing missing provenance.""" + from research_repo_tools.criterion import collect_sample, compare_samples, render_comparison # noqa: PLC0415 -def render_and_promote_artifacts(*, artifacts: ArtifactPaths, output: Path, current: Path, archive_dir: Path) -> ReportId: - """Read historical CSV/JSON through its original schema; never relabel hashes.""" - bundle = load_bundle(artifacts) - release = bundle.context.release - if release.current == release.baseline: - msg = "cannot promote a same-version local comparison as release documentation" - raise ValueError(msg) - text = render_release_artifacts(artifacts).encode() - root = current.parent.parent.resolve() - outputs = {current.relative_to(root).as_posix(): text, output.relative_to(root).as_posix(): text} - if current.exists() and current.read_bytes() != text: - identity = parse_report_id(current.read_text(encoding="utf-8")) - if identity != ReportId(release.current, release.baseline): - archived = archive_dir / f"{identity.current_tag}-vs-{identity.baseline_tag}.md" - outputs[archived.relative_to(root).as_posix()] = current.read_bytes() - inputs = {path.relative_to(root).as_posix(): path.read_bytes() for path in (artifacts.csv, artifacts.provenance)} - publish_publication(plan_outputs(root, outputs, inputs=inputs, immutable=tuple(name for name in outputs if name.startswith("docs/archive/")))) - return ReportId(release.current, release.baseline) - - -def compare_assets(root: Path, pair: ReleasePair, output: Path, suite: str, scope: str) -> None: - """Read published raw archives with shared bounded download/extraction APIs.""" + samples = {} with tempfile.TemporaryDirectory(prefix="la-stack-assets-") as directory: parent = Path(directory) - criterion = parent / "criterion" for phase, tag in (("baseline", pair.baseline), ("current", pair.current)): asset = parent / f"{tag}.tar.gz" download_release_asset(root, "acgetchell/la-stack", tag, f"la-stack-{tag}-criterion-baseline.tar.gz", asset) extracted = parent / phase extract_archive(asset, extracted) - # Preserve native measurements, explicitly retaining their original - # per-release harnesses; these cannot become a complete common run. - sample = tag - destination_sample = "new" if phase == "current" else tag - for source in (extracted / "criterion").glob(f"**/{sample}/*.json"): - target = criterion / source.relative_to(extracted / "criterion").parent.parent / destination_sample / source.name - replace_many({target: source.read_bytes()}) - text = render_historical_assets(criterion, pair, suite, scope) + samples[phase] = collect_sample(extracted / "criterion", tag) + comparison = compare_samples(samples["baseline"], samples["current"]) + if not comparison.comparisons: + msg = "release archives have no common benchmark rows" + raise ValueError(msg) + text = ( + f"# Published release timings\n\nBaseline: {pair.baseline}; current: {pair.current}.\n\n" + "Each archive used its original release harness. Source, host and gate provenance are not recorded; " + "ratios are descriptive and require independent hardware/workload review.\n\n" + render_comparison(comparison) + ).encode() publish_publication(plan_outputs(root, {output.relative_to(root).as_posix(): text}, inputs={"Cargo.toml": (root / "Cargo.toml").read_bytes()})) +def check_release_sample(root: Path, name: str) -> int: + """Require canonical report identities and complete, matching saved bytes.""" + from research_repo_tools.criterion_baselines import load_saved_sample, validate_saved_sample # noqa: PLC0415 + + plan = load_saved_sample(root, "tooling/criterion-saved.toml") + required = required_report_ids(root) + if not required <= set(plan.policy.expected): + msg = "release inventory omits report consumers" + raise ValueError(msg) + saved = validate_saved_sample(Path(os.environ.get("CRITERION_HOME", str(root / plan.criterion_dir))), name, plan.policy, raw_files=plan.raw_files) + for case in saved.cases: + if case.full_id in required and case.storage_path != case.full_id: + msg = f"non-canonical report benchmark storage path for {case.full_id}: {case.storage_path}" + raise ValueError(msg) + return len(saved.cases) + + def main(argv: list[str] | None = None) -> int: """Keep routine performance commands thin and explicit.""" parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("command", choices=("local", "release", "report", "assets")) - parser.add_argument("current", nargs="?") - parser.add_argument("baseline", nargs="?") - parser.add_argument("--suite", choices=("all", "exact", "vs_linalg"), default="all") - parser.add_argument("--scope", choices=("release-signal", "all-benches"), default="release-signal") - parser.add_argument("--stem", type=Path, default=Path("target/bench-reports/performance")) + commands = parser.add_subparsers(dest="command", required=True) + for name in ("local", "release", "assets"): + command = commands.add_parser(name) + command.add_argument("current", nargs="?") + command.add_argument("baseline", nargs="?") + if name != "assets": + command.add_argument("--suite", choices=("all", "exact", "vs_linalg"), default="all") + command.add_argument("--scope", choices=("release-signal", "all-benches"), default="release-signal") + command.add_argument("--stem", type=Path, default=Path("target/bench-reports/performance")) + commands.add_parser("report").add_argument("--stem", type=Path, default=Path("target/bench-reports/performance")) + commands.add_parser("release-check").add_argument("current", help="Saved Criterion sample name") args = parser.parse_args(argv) - root = find_project_root(Path.cwd()).resolve() - stem = root / args.stem try: + root = find_project_root(Path.cwd()).resolve() + if args.command == "release-check": + print(f"Verified {check_release_sample(root, args.current)} complete saved Criterion benchmarks") + return 0 if args.command == "report": - if args.current or args.baseline: - parser.error("report reads retained evidence and takes no release labels") - if any(path.exists() or path.is_symlink() for path in (*scratch_paths(stem), root / archive_directory(root))): - promote(root, stem) - else: - paths = resolve_report_paths(root, ArtifactPaths(stem.with_suffix(".csv"), stem.with_suffix(".provenance.json"))) - render_and_promote_artifacts( - artifacts=paths, output=stem.with_suffix(".md"), current=root / "docs/performance.md", archive_dir=root / "docs/archive/performance" - ) + promote(root, root / args.stem) return 0 modes: dict[str, PairMode] = {"local": "current-vs-latest", "release": "infer-release", "assets": "published-latest"} mode: PairMode = "explicit" if args.current or args.baseline else modes[args.command] pair = select_pair(root, mode, args.current or None, args.baseline or None, local=args.command == "local") if args.command == "assets": - compare_assets(root, pair, root / "target/bench-reports/github-assets-performance.md", args.suite, args.scope) + compare_assets(root, pair, root / "target/bench-reports/github-assets-performance.md") else: + stem = root / args.stem if args.command == "release" and pair.current == pair.baseline: parser.error("release requires distinct release labels") evidence = measure(root, pair, args.suite, args.scope) @@ -225,5 +215,158 @@ def main(argv: list[str] | None = None) -> int: return 1 +PLOT_CONFIG = "tooling/performance-plot.toml" + + +def _repo_root() -> Path: + """Resolve installed commands against the caller's checkout.""" + return find_project_root(Path.cwd()).resolve() + + +def presentation(root: Path, metric_name: str, statistic: Literal["mean", "median"], *, log_y: bool) -> tuple[RunLayout, CoordinatePlot]: + """Map the scientific catalog onto the shared coordinate presentation plan.""" + config = tomllib.loads((root / PLOT_CONFIG).read_text(encoding="utf-8")) + if ( + type(config["schema"]) is not int + or config["schema"] != 1 + or tuple(config["dimensions"]) != scientific_policy(root).dimensions + or config["unit"] != "ns" + ): + msg = "dimension publication requires the canonical D=2-64 selection" + raise ValueError(msg) + catalog = scientific_policy(root) + metric = catalog.metrics[metric_name] + benches = dict(zip(("la-stack current", "nalgebra", "faer"), metric.benchmarks, strict=True)) + selected = tuple( + SeriesSelection(item["name"], item["label"], tuple((str(d), f"d{d}/{benches[item['name']]}") for d in catalog.dimensions)) for item in config["series"] + ) + if tuple(item.name for item in selected) != tuple(benches): + msg = "dimension publication requires current la-stack and both baseline peer views" + raise ValueError(msg) + layout = RunLayout( + selected, + tuple(RunRow(str(d), str(d), d) for d in catalog.dimensions), + statistic, + config["unit"], + comparisons=(("nalgebra", "la-stack current"), ("faer", "la-stack current")), + ) + settings = config["plot"] + plot = CoordinatePlot( + f"{metric.title}: {statistic} time vs dimension", settings["x-label"], settings["y-label"], settings["x-scale"], "log" if log_y else "linear" + ) + return layout, plot + + +def _asset_links(document: bytes, *, metric: str, statistic: str, release: str) -> bytes: + """Advance precisely the three existing la-stack artifact pointers.""" + stem = f"/docs/assets/bench/vs_linalg_{metric}_{statistic}" + pattern = re.compile( + rb"(?Phttps://(?:github\.com/acgetchell/la-stack/(?:blob|raw|tree)/|raw\.githubusercontent\.com/acgetchell/la-stack/))" + rb"v[0-9]+\.[0-9]+\.[0-9]+" + rb"(?P" + re.escape(stem.encode()) + rb"(?:\.csv|\.provenance\.json|\.svg))" + ) + found = sorted(match["asset"] for match in pattern.finditer(document)) + if found != sorted(f"{stem}{suffix}".encode() for suffix in (".csv", ".svg", ".provenance.json")): + msg = "README requires exactly one tagged pointer to each selected artifact" + raise ValueError(msg) + return pattern.sub(lambda match: match["prefix"] + release.encode() + match["asset"], document) + + +def publish_plot(root: Path, metric: str, statistic: Literal["mean", "median"], *, log_y: bool, update_readme: bool) -> None: + """Retain original phases, enforce scientific eligibility, and publish once.""" + # The report registry imports only the independent benchmark catalog. + import performance_runs as policy # noqa: PLC0415 + + stem = root / "target/bench-reports/performance" + evidence = policy.retained_run(root, stem) + run = policy.validate_scientific_run(evidence, release=update_readme, root=root) + verified_inputs = policy.verify_current_source(root, evidence) if update_readme else {} + layout, plot = presentation(root, metric, statistic, log_y=log_y) + phases = dict(evidence.sources) + current_context, peer_context = dict(phases["current"].context), dict(phases["baseline"].context) + versions = {"la-stack current": current_context["release"], "nalgebra": peer_context["dependency.nalgebra"], "faer": peer_context["dependency.faer"]} + layout = replace(layout, series=tuple(replace(item, label=f"{item.label} ({versions[item.name]})") for item in layout.series)) + svg = render_coordinate_svg(evidence, layout, plot) + csv = serialize_run_csv(evidence, layout) + identity = run_identity(evidence) + payload, manifest = policy.scratch_paths(stem) + from_archive = not payload.exists() + inputs = { + PLOT_CONFIG: (root / PLOT_CONFIG).read_bytes(), + policy.POLICY_CONFIG: (root / policy.POLICY_CONFIG).read_bytes(), + policy.REPORT_CONFIG: (root / policy.REPORT_CONFIG).read_bytes(), + **verified_inputs, + } + archive = root / policy.archive_directory(root) + if not from_archive: + inputs.update(zip((payload.relative_to(root).as_posix(), manifest.relative_to(root).as_posix()), serialize_evidence(evidence), strict=True)) + # README provenance must remain valid after scratch cleanup. Require the + # selected measurement to have been promoted before referencing its archive. + use_archive = from_archive or update_readme + if use_archive: + if not from_archive and run_identity(load_latest_run(root, policy.archive_directory(root))) != identity: + msg = "README evidence differs from the retained latest run; run performance-doc first" + raise ValueError(msg) + payload, manifest = archive / "runs" / identity / "run.json", archive / "runs" / identity / "evidence.json" + inputs.update({path.relative_to(root).as_posix(): path.read_bytes() for path in archive.rglob("*") if path.is_file()}) + inputs.update(zip((payload.relative_to(root).as_posix(), manifest.relative_to(root).as_posix()), serialize_evidence(evidence), strict=True)) + prefix = f"docs/assets/bench/vs_linalg_{metric}_{statistic}" + provenance = { + "schema": 4, + "artifact": "la-stack dimension plot", + "run_id": identity, + "payload": payload.relative_to(root).as_posix(), + "manifest": manifest.relative_to(root).as_posix(), + "statistic": statistic, + "unit": layout.unit, + "series": {item.name: item.phase for item in run.series if item.name in {selected.name for selected in layout.series}}, + } + outputs = {f"{prefix}.svg": svg, f"{prefix}.csv": csv, f"{prefix}.provenance.json": (json.dumps(provenance, indent=2, sort_keys=True) + "\n").encode()} + git_checks = () + document: bytes | None = None + if update_readme: + context = dict(dict(evidence.sources)["current"].context) + markers = MarkerPair(f"", f"") + document = (root / "README.md").read_bytes() + git_checks = (GitCheck(context["release"], tuple(outputs), allow_missing=True),) + outputs["README.md"] = _asset_links( + replace_section(document, markers, render_run_table(evidence, layout)), metric=metric, statistic=statistic, release=context["release"] + ) + plan = plan_outputs(root, outputs, inputs=inputs) + if update_readme and dict(plan.originals)["README.md"] != document: + msg = "README changed during rendering" + raise ValueError(msg) + checks = () + if update_readme: + config = policy.configuration(root) + checks = tuple( + GlobCheck(patterns, tuple(json.loads(current_context[field]))) + for field, patterns in (("source-inventory", config.sources), ("harness-inventory", config.harness)) + ) + # Preserve the shared planner's snapshots and add its public freshness + # checks for the independently validated consumer selections. + plan = replace( + plan, git_checks=git_checks, glob_checks=checks, run_checks=(RunSelectionCheck(policy.archive_directory(root), identity),) if use_archive else () + ) + publish_publication(plan) + + +def dimension_main(argv: list[str] | None = None) -> int: + """Plot complete evidence or publish its canonical README section.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--metric", default="lu_solve", help="Metric declared in tooling/performance-policy.toml") + parser.add_argument("--stat", choices=("mean", "median"), default="median") + parser.add_argument("--sample", choices=("new",), default="new", help="Retained current series; raw Criterion samples are no longer publication inputs.") + parser.add_argument("--log-y", action="store_true") + parser.add_argument("--update-readme", action="store_true") + args = parser.parse_args(argv) + try: + publish_plot(_repo_root(), args.metric, args.stat, log_y=args.log_y, update_readme=args.update_readme) + return 0 + except (OSError, ValueError, TypeError, KeyError, ExecutableNotFoundError, subprocess.SubprocessError, ExceptionGroup) as error: + print(f"dimension publication: {format_exception_diagnostics(error)}", file=sys.stderr) + return 2 + + if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(dimension_main(sys.argv[2:]) if sys.argv[1:2] == ["plot"] else main()) diff --git a/scripts/bench_compare.py b/scripts/bench_compare.py deleted file mode 100644 index 690b85e..0000000 --- a/scripts/bench_compare.py +++ /dev/null @@ -1,2157 +0,0 @@ -#!/usr/bin/env python3 -"""Compare la-stack benchmark results across Criterion baselines. - -Reads Criterion output under: - target/criterion/{group}/{bench}/{sample}/estimates.json - -Writes a local Markdown performance report and, for release workflows, a -schema-versioned CSV plus JSON provenance pair that can reproduce it. - -Typical workflow (see docs/RELEASING.md): - - # Save a full baseline for the last release - just bench-save-last - - # ... make optimisations ... - - # Run the cheap latest-vs-last workflow - just bench-latest-vs-last - - # Or just render the report from existing Criterion output - just bench-compare -""" - -import argparse -import json -import re -import subprocess -import sys -import tomllib -from collections.abc import Mapping -from dataclasses import dataclass -from datetime import UTC, datetime -from pathlib import Path -from typing import TYPE_CHECKING, Literal, Protocol, cast - -from research_repo_tools.criterion import Comparison as TimingComparison, Estimate, read_estimate -from research_repo_tools.files import replace_many -from research_repo_tools.process import ExecutableNotFoundError, format_exception_diagnostics, run_command - -from benchmark_contract import find_project_root -from criterion_dim_plot import METRICS -from performance_artifacts import ( - PRE_RATIONAL_INPUT_API_COMPATIBILITY, - ArtifactContext, - ArtifactPaths, - PerformanceBundle, - PerformanceRow, - ReleasePair, - ReportSource, - TimingEstimate, - bundle_outputs, - ensure_distinct_paths, - freeze_mapping, - load_bundle, - resolve_shared_harness_compatibility, -) - -if TYPE_CHECKING: - from research_repo_tools.release_pairs import ReleasePair as SharedReleasePair - -# --------------------------------------------------------------------------- -# Benchmark group / bench discovery -# --------------------------------------------------------------------------- - -# Groups and the benchmarks within each group that we track. -# -# Mirrors the structure of `benches/exact.rs`: general-case per-dimension -# groups (`exact_d{2..5}`), fixed-seed full-corpus groups, rational-input -# comparisons (`rational_input_d{2..8}`), plus adversarial/extreme-input groups -# that share a fixed five-bench layout -# (`det_sign_exact`, `det_exact`, `solve_exact`, `solve_exact_f64_result`, -# `solve_exact_rounded_f64`). -_EXTREME_BENCHES: list[str] = [ - "det_sign_exact", - "det_exact", - "solve_exact", - "solve_exact_f64_result", - "solve_exact_rounded_f64", -] -_RANDOM_CORPUS_BENCHES: list[str] = _EXTREME_BENCHES.copy() - -_EXACT_DIMENSION_BENCHES: list[str] = [ - "det", - "det_exact", - "det_exact_f64_result", - "det_exact_rounded_f64", - "det_sign_exact", - "solve_exact", - "solve_exact_f64_result", - "solve_exact_rounded_f64", -] -_EXACT_DIMENSION_BENCHES_WITH_DIRECT: list[str] = [ - "det", - "det_direct", - "det_direct_with_errbound", - "det_errbound", - *_EXACT_DIMENSION_BENCHES[1:], -] - -_RATIONAL_INPUT_BENCHES: list[str] = [ - "det_sign_row_cleared_bareiss", - "det_row_cleared_bareiss", - "det_big_rational_gaussian", - "solve_row_cleared_bareiss", - "solve_big_rational_gaussian", -] - -EXACT_GROUPS: dict[str, list[str]] = { - "exact_d2": _EXACT_DIMENSION_BENCHES_WITH_DIRECT, - "exact_d3": _EXACT_DIMENSION_BENCHES_WITH_DIRECT, - "exact_d4": _EXACT_DIMENSION_BENCHES_WITH_DIRECT, - "exact_d5": _EXACT_DIMENSION_BENCHES, - "exact_random_corpus_d2": _RANDOM_CORPUS_BENCHES, - "exact_random_corpus_d3": _RANDOM_CORPUS_BENCHES, - "exact_random_corpus_d4": _RANDOM_CORPUS_BENCHES, - "exact_random_corpus_d5": _RANDOM_CORPUS_BENCHES, - **{f"rational_input_d{dimension}": _RATIONAL_INPUT_BENCHES for dimension in range(2, 9)}, - "exact_near_singular_3x3": _EXTREME_BENCHES, - "exact_large_entries_3x3": _EXTREME_BENCHES, - "exact_hilbert_4x4": _EXTREME_BENCHES, - "exact_hilbert_5x5": _EXTREME_BENCHES, -} - -EXACT_RELEASE_SIGNAL_GROUPS: frozenset[str] = frozenset(EXACT_GROUPS) - -VS_LINALG_LA_STACK_ONLY_BENCHES_BY_METRIC: dict[str, list[str]] = { - "det_via_lu": ["la_stack_det"], -} -VS_LINALG_EXTRA_PEER_BENCHES_BY_METRIC: dict[str, list[tuple[str, str, str]]] = { - "lu": [("la_stack_ldlt", "nalgebra_cholesky", "faer_ldlt")], - "lu_solve": [("la_stack_ldlt_solve", "nalgebra_cholesky_solve", "faer_ldlt_solve")], - "solve_from_lu": [("la_stack_solve_from_ldlt", "nalgebra_solve_from_cholesky", "faer_solve_from_ldlt")], - "det_from_lu": [("la_stack_det_from_ldlt", "nalgebra_det_from_cholesky", "faer_det_from_ldlt")], -} -VS_LINALG_STANDARD_BENCH_ORDER: list[str] = [ - bench - for metric_key, metric in METRICS.items() - for bench in ( - [ - metric.la_bench, - *VS_LINALG_LA_STACK_ONLY_BENCHES_BY_METRIC.get(metric_key, []), - metric.na_bench, - metric.fa_bench, - *[peer_bench for peer_group in VS_LINALG_EXTRA_PEER_BENCHES_BY_METRIC.get(metric_key, []) for peer_bench in peer_group], - ] - ) -] - -VS_LINALG_D8_RELEASE_SIGNAL_BENCHES: list[str] = [ - "la_stack_lu_pivoting", - "la_stack_lu_ill_conditioned", - "la_stack_ldlt_ill_conditioned", - "la_stack_det_from_lu_balanced_range", - "la_stack_det_from_ldlt_balanced_range", -] -_PRE_RATIONAL_INPUT_API_COMPATIBILITY = PRE_RATIONAL_INPUT_API_COMPATIBILITY -_RATIONAL_INPUT_ROWS: frozenset[tuple[str, str]] = frozenset( - (group, bench) for group, benches in EXACT_GROUPS.items() if group.startswith("rational_input_d") for bench in benches -) -VS_LINALG_RELEASE_SIGNAL_BENCHES_BY_DIM: dict[int, list[str]] = { - 8: VS_LINALG_D8_RELEASE_SIGNAL_BENCHES, -} -VS_LINALG_CANONICAL_DIMS: tuple[int, ...] = (2, 3, 4, 5, 8, 16, 32, 64) -VS_LINALG_BENCH_ORDER: list[str] = [ - *VS_LINALG_STANDARD_BENCH_ORDER, - *VS_LINALG_D8_RELEASE_SIGNAL_BENCHES, -] - -VS_LINALG_LA_STACK_BENCHES: frozenset[str] = frozenset(bench for bench in VS_LINALG_STANDARD_BENCH_ORDER if bench.startswith("la_stack_")) -VS_LINALG_BASELINE_PEERS: dict[str, tuple[str, str]] = {metric.la_bench: (metric.na_bench, metric.fa_bench) for metric in METRICS.values()} -VS_LINALG_BASELINE_PEERS.update( - { - la_stack_bench: (nalgebra_bench, faer_bench) - for peer_groups in VS_LINALG_EXTRA_PEER_BENCHES_BY_METRIC.values() - for la_stack_bench, nalgebra_bench, faer_bench in peer_groups - } -) - -SUITE_CHOICES: tuple[str, ...] = ("all", "exact", "vs_linalg") -SCOPE_CHOICES: tuple[str, ...] = ("release-signal", "all-benches") - -HOW_TO_UPDATE_SECTION = """## How to Update - -Local performance reports are generated in isolated temporary worktrees: - -```bash -# Local development: compare the current tree with the latest release -just performance-local - -# Release PR: update docs/performance.md and archive the previous report -just performance-release - -# Build release docs from retained CSV/JSON inputs (no benchmarks) -just performance-doc - -# GitHub Actions release assets -just performance-github-assets - -# Explicit repair -just performance-release -``` - -`just performance-local` writes `performance.md` plus retained `performance.csv` and -`performance.provenance.json` comparison inputs under `target/bench-reports/` without promoting documentation. -It applies staged and unstaged tracked changes; untracked files are excluded. -`just performance-github-assets` writes `target/bench-reports/github-assets-performance.md`. -`just performance-release` also preserves every recorded local benchmark summary and the report inputs under -`docs/performance/-vs-//`, then promotes distinct-release documentation. -`just performance-doc` consumes the retained pair from either workflow without benchmarking and promotes it when the package versions differ. -After `just clean`, `performance-doc` and `performance-readme` use the latest complete snapshot in `docs/performance/`. -For a distinct pair, `performance-local` followed by `performance-doc` is equivalent to the atomic `performance-release` workflow. - -See the [local summary index](https://github.com/acgetchell/la-stack/tree/main/docs/performance) -for the saved-data schema, provenance, and historical availability. -Older curated release-to-release reports are archived in `docs/archive/performance/`. - -See `docs/BENCHMARKING.md` for the full comparison workflow. -""" - -type ChangeAssessment = Literal["improvement", "regression", "inconclusive", "unknown"] -type BenchmarkSuite = Literal["all", "exact", "vs_linalg"] -type ComparisonScope = Literal["release-signal", "all-benches"] -type Statistic = Literal["mean", "median"] - - -@dataclass(frozen=True, slots=True) -class BenchResult: - """A single benchmark measurement (point estimate + confidence interval).""" - - suite: str - group: str - bench: str - estimate: Estimate - - -@dataclass(frozen=True, slots=True) -class Comparison: - """A comparison between baseline and current benchmark results.""" - - suite: str - group: str - bench: str - baseline: Estimate - current: Estimate - assessment: ChangeAssessment - baseline_bench: str | None = None - baseline_nalgebra: Estimate | None = None - baseline_faer: Estimate | None = None - - @property - def baseline_ns(self) -> float: - """Return the baseline point estimate.""" - return self.baseline.point - - @property - def current_ns(self) -> float: - """Return the current point estimate.""" - return self.current.point - - @property - def speedup(self) -> float: - """Return baseline/current, where values above one are faster.""" - return TimingComparison(self.bench, self.baseline, self.current).speedup - - @property - def pct_change(self) -> float: - """Return signed point-estimate change, where negative is faster.""" - return 0.0 - TimingComparison(self.bench, self.baseline, self.current).percent_reduction - - @property - def baseline_nalgebra_ns(self) -> float | None: - """Return the baseline nalgebra point estimate, when available.""" - return None if self.baseline_nalgebra is None else self.baseline_nalgebra.point - - @property - def baseline_faer_ns(self) -> float | None: - """Return the baseline faer point estimate, when available.""" - return None if self.baseline_faer is None else self.baseline_faer.point - - -@dataclass(frozen=True, slots=True) -class CoverageGap: - """An expected comparison row missing one or both Criterion samples.""" - - suite: str - group: str - bench: str - baseline_bench: str - missing_current: bool - missing_baseline: bool - - -@dataclass(frozen=True, slots=True) -class ComparisonCollection: - """Complete comparisons plus deterministic coverage gaps.""" - - comparisons: list[Comparison] - gaps: list[CoverageGap] - - -@dataclass(frozen=True, slots=True) -class ComparisonPolicy: - """Coverage policy for one baseline comparison.""" - - scope: str = "release-signal" - baseline_api_compatibility: str | None = None - shared_harness_rational_inputs: bool = True - - -_DEFAULT_COMPARISON_POLICY = ComparisonPolicy() - - -@dataclass(frozen=True, slots=True) -class CriterionSelection: - """Requested Criterion settings that provenance must describe exactly.""" - - suite: BenchmarkSuite - scope: ComparisonScope - statistic: Statistic - sample: str - - -@dataclass(frozen=True, slots=True) -class HarnessProvenance: - """Validated benchmark measurement and correctness provenance.""" - - schema: int - mode: str - sha256: str | None - baseline: str - current: str | None = None - measurement: Mapping[str, object] | None = None - publication: Mapping[str, object] | None = None - criterion: CriterionProvenance | None = None - validation: Mapping[str, object] | None = None - - def __post_init__(self) -> None: - """Detach and freeze nested provenance so the validated model stays valid.""" - for field in ("measurement", "publication", "validation"): - value = getattr(self, field) - if value is not None: - object.__setattr__(self, field, freeze_mapping(value)) - - -@dataclass(frozen=True, slots=True) -class CriterionProvenance: - """Validated Criterion settings and commands recorded for one report.""" - - suite: BenchmarkSuite - scope: ComparisonScope - statistic: Statistic - sample: str - criterion_version: str - baseline_command: tuple[str, ...] - current_command: tuple[str, ...] - - -@dataclass(frozen=True, slots=True) -class ReportSettings: - """Settings rendered into the benchmark report header.""" - - baseline_name: str | None - stat: Statistic - suite: BenchmarkSuite - scope: ComparisonScope - harness_provenance: HarnessProvenance | None = None - - -# --------------------------------------------------------------------------- -# Criterion JSON parsing -# --------------------------------------------------------------------------- - - -def _repo_root() -> Path: - """Resolve the checkout from the caller's working tree, including wheel installs.""" - return find_project_root() - - -def _dim_from_vs_linalg_group(name: str) -> int | None: - """Parse a vs_linalg Criterion group name such as d2 or d64.""" - if not name.startswith("d"): - return None - suffix = name.removeprefix("d") - if not suffix.isdecimal(): - return None - return int(suffix) - - -def _comparison_row_suite(group: str) -> Literal["exact", "vs_linalg"]: - """Return the benchmark suite that owns one comparison row.""" - if group in EXACT_GROUPS: - return "exact" - if _dim_from_vs_linalg_group(group) is not None: - return "vs_linalg" - msg = f"unsupported comparison group: {group!r}" - raise ValueError(msg) - - -def _is_selected_comparison_row( - group: str, - bench: str, - *, - suite: str, - scope: str, -) -> bool: - """Return whether a comparison row belongs to the requested suite and scope.""" - row_suite = _comparison_row_suite(group) - if suite not in ("all", row_suite): - return False - if scope != "release-signal": - return True - if row_suite == "exact": - return group in EXACT_RELEASE_SIGNAL_GROUPS - - dim = _dim_from_vs_linalg_group(group) - return dim is not None and (bench in VS_LINALG_LA_STACK_BENCHES or bench in VS_LINALG_RELEASE_SIGNAL_BENCHES_BY_DIM.get(dim, [])) - - -def _read_harness_provenance( - criterion_dir: Path, - *, - expected_baseline: str, - expected: CriterionSelection, -) -> HarnessProvenance | None: - """Read provenance tied to the exact Criterion samples being compared.""" - provenance_path = criterion_dir / ".la-stack-benchmark-harness.json" - if not provenance_path.exists(): - return None - - try: - data = json.loads(provenance_path.read_text(encoding="utf-8")) - except json.JSONDecodeError as err: - msg = f"malformed benchmark harness provenance JSON in {provenance_path}: {err}" - raise ValueError(msg) from err - if not isinstance(data, dict): - msg = f"expected JSON object in {provenance_path}" - raise TypeError(msg) - - return _parse_harness_provenance( - data, - path=provenance_path, - expected_baseline=expected_baseline, - expected=expected, - ) - - -def _parse_harness_provenance( - data: Mapping[str, object], - *, - path: Path, - expected_baseline: str, - expected: CriterionSelection, -) -> HarnessProvenance: - """Parse already-decoded harness provenance at a trusted report boundary.""" - - schema = data.get("schema") - mode_value = data.get("mode") - if not isinstance(mode_value, str): - msg = f"unsupported or missing mode in {path}: {mode_value!r}" - raise TypeError(msg) - mode = mode_value - baseline = _required_metadata_string(data, "baseline", path) - if baseline != expected_baseline: - msg = f"benchmark harness provenance baseline {baseline!r} does not match requested Criterion baseline {expected_baseline!r} in {path}" - raise ValueError(msg) - - if not isinstance(schema, int) or isinstance(schema, bool): - msg = f"unsupported or missing schema in {path}: expected integer 1 or 2, got {schema!r}" - raise TypeError(msg) - - if schema == 1: - if mode != "shared-current-harness": - msg = f"unsupported or missing mode in {path}: {mode!r}" - raise ValueError(msg) - sha256 = _required_sha256(data, "sha256", path) - return HarnessProvenance(schema=1, mode=mode, sha256=sha256, baseline=baseline) - - if schema != 2: - msg = f"unsupported or missing schema in {path}: expected 1 or 2, got {schema!r}" - raise ValueError(msg) - if mode not in {"shared-current-harness", "historical-assets"}: - msg = f"unsupported or missing mode in {path}: {mode!r}" - raise ValueError(msg) - current = _required_metadata_string(data, "current", path) - - measurement = _required_metadata_object(data, "measurement", path) - publication = _required_metadata_object(data, "publication", path) - criterion_data = _required_metadata_object(data, "criterion", path) - validation = _required_metadata_object(data, "validation", path) - _validate_measurement_metadata(measurement, mode=mode, path=path) - _validate_environment_metadata(publication, path=path, context="publication") - criterion = _parse_criterion_metadata( - criterion_data, - path=path, - expected=expected, - ) - _validate_validation_metadata(validation, path=path) - _validate_baseline_api_compatibility(validation, current=current, baseline=baseline, path=path) - _validate_schema2_consistency( - mode=mode, - measurement=measurement, - publication=publication, - validation=validation, - path=path, - ) - - sha256: str | None = None - if measurement.get("status") == "recorded": - sha256 = _required_sha256(measurement, "harness_sha256", path) - return HarnessProvenance( - schema=2, - mode=mode, - sha256=sha256, - baseline=baseline, - current=current, - measurement=measurement, - publication=publication, - criterion=criterion, - validation=validation, - ) - - -def _required_metadata_object(data: Mapping[str, object], field: str, path: Path) -> Mapping[str, object]: - """Return a required provenance object with contextual diagnostics.""" - value = data.get(field) - if not isinstance(value, Mapping) or not all(isinstance(key, str) for key in value): - msg = f"invalid or missing {field} object in {path}" - raise ValueError(msg) - return cast("Mapping[str, object]", value) - - -def _required_metadata_string(data: Mapping[str, object], field: str, path: Path) -> str: - """Return a required non-empty provenance string.""" - value = data.get(field) - if not isinstance(value, str) or not value.strip(): - msg = f"invalid or missing {field} in {path}" - raise ValueError(msg) - return value - - -def _required_sha256(data: Mapping[str, object], field: str, path: Path) -> str: - """Return a required lowercase SHA-256 digest.""" - value = data.get(field) - if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None: - msg = f"invalid or missing {field} in {path}: expected 64 lowercase hexadecimal characters" - raise ValueError(msg) - return value - - -def _validate_environment_metadata(data: Mapping[str, object], *, path: Path, context: str) -> None: - """Validate deterministic environment fields used to reproduce a run.""" - for field in ("cpu", "os", "rustc", "commit"): - _required_metadata_string(data, field, path) - _required_sha256(data, "cargo_lock_sha256", path) - _required_sha256(data, "harness_sha256", path) - _required_sha256(data, "source_state_sha256", path) - if not isinstance(data.get("git_clean"), bool): - msg = f"invalid or missing {context}.git_clean in {path}" - raise TypeError(msg) - gate = _required_metadata_string(data, "correctness_gate", path) - if gate != "passed": - msg = f"{context}.correctness_gate in {path} must be 'passed', got {gate!r}" - raise ValueError(msg) - - -def _validate_measurement_metadata(data: Mapping[str, object], *, mode: object, path: Path) -> None: - """Validate recorded or explicitly unavailable measurement provenance.""" - status = _required_metadata_string(data, "status", path) - if status == "recorded": - if mode != "shared-current-harness": - msg = f"recorded measurement provenance in {path} requires shared-current-harness mode" - raise ValueError(msg) - for field in ("cpu", "os", "rustc", "current_commit", "baseline_commit"): - _required_metadata_string(data, field, path) - if cast("str", data["cpu"]).casefold() == "unavailable": - msg = f"recorded measurement provenance in {path} requires an identified CPU model" - raise ValueError(msg) - _required_sha256(data, "cargo_lock_sha256", path) - _required_sha256(data, "harness_sha256", path) - _required_sha256(data, "current_source_state_sha256", path) - _required_sha256(data, "baseline_source_state_sha256", path) - for field in ("current_git_clean", "baseline_git_clean"): - if not isinstance(data.get(field), bool): - msg = f"invalid or missing measurement.{field} in {path}" - raise TypeError(msg) - return - if status == "unavailable": - _required_metadata_string(data, "reason", path) - return - msg = f"unsupported measurement.status in {path}: {status!r}" - raise ValueError(msg) - - -def _parse_criterion_metadata( - data: Mapping[str, object], - *, - path: Path, - expected: CriterionSelection, -) -> CriterionProvenance: - """Parse Criterion metadata and bind it to the requested report settings.""" - suite = _required_metadata_string(data, "suite", path) - scope = _required_metadata_string(data, "scope", path) - statistic = _required_metadata_string(data, "statistic", path) - sample = _required_metadata_string(data, "sample", path) - criterion_version = _required_metadata_string(data, "criterion_version", path) - - expected_fields = { - "suite": expected.suite, - "scope": expected.scope, - "statistic": expected.statistic, - "sample": expected.sample, - } - observed_fields = { - "suite": suite, - "scope": scope, - "statistic": statistic, - "sample": sample, - } - for field, expected_value in expected_fields.items(): - observed = observed_fields[field] - if observed != expected_value: - msg = f"criterion.{field} {observed!r} in {path} does not match requested value {expected_value!r}" - raise ValueError(msg) - - if suite not in SUITE_CHOICES: - msg = f"unsupported criterion.suite in {path}: {suite!r}" - raise ValueError(msg) - if scope not in SCOPE_CHOICES: - msg = f"unsupported criterion.scope in {path}: {scope!r}" - raise ValueError(msg) - if statistic not in {"mean", "median"}: - msg = f"unsupported criterion.statistic in {path}: {statistic!r}" - raise ValueError(msg) - - commands: dict[str, tuple[str, ...]] = {} - for field in ("baseline_command", "current_command"): - value = data.get(field) - if not isinstance(value, (list, tuple)) or not value or not all(isinstance(part, str) and part for part in value): - msg = f"invalid or missing criterion.{field} in {path}" - raise ValueError(msg) - commands[field] = tuple(cast("list[str] | tuple[str, ...]", value)) - - return CriterionProvenance( - suite=cast("BenchmarkSuite", suite), - scope=cast("ComparisonScope", scope), - statistic=statistic, - sample=sample, - criterion_version=criterion_version, - baseline_command=commands["baseline_command"], - current_command=commands["current_command"], - ) - - -def _validate_validation_metadata(data: Mapping[str, object], *, path: Path) -> None: - """Require fixture validation for both compared revisions.""" - command = data.get("command") - if command not in (["just", "test-bench-inputs"], ("just", "test-bench-inputs")): - msg = f"invalid validation.command in {path}: expected ['just', 'test-bench-inputs']" - raise ValueError(msg) - for field in ("current_revision", "baseline_revision"): - value = _required_metadata_string(data, field, path) - if value != "passed": - msg = f"validation.{field} in {path} must be 'passed', got {value!r}" - raise ValueError(msg) - for field in ("current_commit", "baseline_commit"): - _required_metadata_string(data, field, path) - for field in ("current_source_state_sha256", "baseline_source_state_sha256"): - _required_sha256(data, field, path) - for field in ("current_git_clean", "baseline_git_clean"): - if not isinstance(data.get(field), bool): - msg = f"invalid or missing validation.{field} in {path}" - raise TypeError(msg) - _required_metadata_string(data, "baseline_api_compatibility", path) - if not isinstance(data.get("shared_harness_rational_inputs"), bool): - msg = f"invalid or missing validation.shared_harness_rational_inputs in {path}" - raise TypeError(msg) - - -def _validate_baseline_api_compatibility( - data: Mapping[str, object], - *, - current: str, - baseline: str, - path: Path, -) -> None: - """Bind each supported compatibility adapter to its baseline releases.""" - compatibility = data.get("baseline_api_compatibility") - shared_harness_rational_inputs = data.get("shared_harness_rational_inputs") - if not isinstance(shared_harness_rational_inputs, bool): - msg = f"invalid or missing validation.shared_harness_rational_inputs in {path}" - raise TypeError(msg) - expected = resolve_shared_harness_compatibility( - current=current, - baseline=baseline, - shared_harness_rational_inputs=shared_harness_rational_inputs, - ).baseline_api_compatibility - if compatibility != expected: - msg = f"validation.baseline_api_compatibility in {path} must be {expected!r} for baseline {baseline!r}, got {compatibility!r}" - raise ValueError(msg) - - -def _require_matching_fields( - first: Mapping[str, object], - second: Mapping[str, object], - pairs: tuple[tuple[str, str], ...], - *, - contexts: tuple[str, str], - path: Path, -) -> None: - first_context, second_context = contexts - for first_field, second_field in pairs: - if first.get(first_field) != second.get(second_field): - msg = f"{first_context}.{first_field} in {path} does not match {second_context}.{second_field}" - raise ValueError(msg) - - -def _validate_schema2_consistency( - *, - mode: str, - measurement: Mapping[str, object], - publication: Mapping[str, object], - validation: Mapping[str, object], - path: Path, -) -> None: - """Reject internally contradictory measurement and validation evidence.""" - status = measurement.get("status") - expected_status = "recorded" if mode == "shared-current-harness" else "unavailable" - if status != expected_status: - msg = f"mode {mode!r} in {path} requires measurement.status {expected_status!r}, got {status!r}" - raise ValueError(msg) - - _require_matching_fields( - publication, - validation, - (("commit", "current_commit"), ("git_clean", "current_git_clean"), ("source_state_sha256", "current_source_state_sha256")), - contexts=("publication", "validation"), - path=path, - ) - - if status != "recorded": - return - identical_fields = tuple((field, field) for field in ("cpu", "os", "rustc", "cargo_lock_sha256", "harness_sha256")) - _require_matching_fields( - measurement, - publication, - (*identical_fields, ("current_commit", "commit"), ("current_git_clean", "git_clean"), ("current_source_state_sha256", "source_state_sha256")), - contexts=("measurement", "publication"), - path=path, - ) - baseline_fields = tuple((field, field) for field in ("baseline_commit", "baseline_git_clean", "baseline_source_state_sha256")) - _require_matching_fields( - measurement, - validation, - baseline_fields, - contexts=("measurement", "validation"), - path=path, - ) - measurement_compatibility = measurement.get("baseline_api_compatibility") - if measurement_compatibility != validation.get("baseline_api_compatibility"): - msg = f"measurement.baseline_api_compatibility in {path} does not match validation.baseline_api_compatibility" - raise ValueError(msg) - - -def assess_change(baseline: Estimate, current: Estimate) -> ChangeAssessment: - """Classify a change conservatively from non-overlapping Criterion intervals.""" - if baseline.lower is None or baseline.upper is None or current.lower is None or current.upper is None: - return "unknown" - if current.upper < baseline.lower: - return "improvement" - if current.lower > baseline.upper: - return "regression" - return "inconclusive" - - -def _collect_exact_results(criterion_dir: Path, sample: str, stat: str) -> list[BenchResult]: - """Collect exact-arithmetic benchmark results from Criterion output.""" - results: list[BenchResult] = [] - - for group, benches in EXACT_GROUPS.items(): - group_dir = criterion_dir / group - if not group_dir.is_dir(): - continue - - for bench in benches: - est_path = group_dir / bench / sample / "estimates.json" - if not est_path.exists(): - continue - - estimate = read_estimate(est_path, statistic=cast("Statistic", stat)) - results.append(BenchResult(suite="exact", group=group, bench=bench, estimate=estimate)) - - return results - - -def _ordered_vs_linalg_benches(group_dir: Path, sample: str) -> list[str]: - """Return present vs_linalg benches in a stable, metric-aware order.""" - present = {child.name for child in group_dir.iterdir() if child.is_dir() and (child / sample / "estimates.json").exists()} - ordered = [bench for bench in VS_LINALG_BENCH_ORDER if bench in present] - extras = sorted(present.difference(VS_LINALG_BENCH_ORDER)) - return [*ordered, *extras] - - -def _collect_vs_linalg_results(criterion_dir: Path, sample: str, stat: str) -> list[BenchResult]: - """Collect vs_linalg benchmark results from Criterion output.""" - results: list[BenchResult] = [] - dim_groups: list[tuple[int, Path]] = [] - - for group_dir in criterion_dir.iterdir(): - if not group_dir.is_dir(): - continue - dim = _dim_from_vs_linalg_group(group_dir.name) - if dim is None: - continue - dim_groups.append((dim, group_dir)) - - for _dim, group_dir in sorted(dim_groups, key=lambda item: item[0]): - for bench in _ordered_vs_linalg_benches(group_dir, sample): - est_path = group_dir / bench / sample / "estimates.json" - estimate = read_estimate(est_path, statistic=cast("Statistic", stat)) - results.append(BenchResult(suite="vs_linalg", group=group_dir.name, bench=bench, estimate=estimate)) - - return results - - -def _collect_results(criterion_dir: Path, sample: str, stat: str, suite: str = "all") -> list[BenchResult]: - """Collect benchmark results from Criterion output.""" - results: list[BenchResult] = [] - if suite in ("all", "exact"): - results.extend(_collect_exact_results(criterion_dir, sample, stat)) - if suite in ("all", "vs_linalg"): - results.extend(_collect_vs_linalg_results(criterion_dir, sample, stat)) - return results - - -def _unavailable_baseline_rows(policy: ComparisonPolicy) -> frozenset[tuple[str, str]]: - """Return rows excluded by the baseline API under the installed shared harness.""" - if policy.shared_harness_rational_inputs and policy.baseline_api_compatibility == _PRE_RATIONAL_INPUT_API_COMPATIBILITY: - return _RATIONAL_INPUT_ROWS - return frozenset[tuple[str, str]]() - - -def _collect_exact_comparisons( - criterion_dir: Path, - baseline_name: str, - stat: str, - suite: str, - policy: ComparisonPolicy, -) -> ComparisonCollection: - """Compare exact results while retaining every missing expected row.""" - comparisons: list[Comparison] = [] - gaps: list[CoverageGap] = [] - unavailable_baseline_rows = _unavailable_baseline_rows(policy) - - selected_groups: list[str] = [] - for group, benches in EXACT_GROUPS.items(): - if group.startswith("rational_input_d") and not policy.shared_harness_rational_inputs: - continue - selected_benches = [bench for bench in benches if _is_selected_comparison_row(group, bench, suite=suite, scope=policy.scope)] - if not selected_benches: - continue - selected_groups.append(group) - - group_dir = criterion_dir / group - for bench in selected_benches: - new_path = group_dir / bench / "new" / "estimates.json" - base_path = group_dir / bench / baseline_name / "estimates.json" - missing_current = not new_path.exists() - missing_baseline = not base_path.exists() - baseline_unavailable = (group, bench) in unavailable_baseline_rows - - if baseline_unavailable: - if missing_current: - gaps.append( - CoverageGap( - suite="exact", - group=group, - bench=bench, - baseline_bench=bench, - missing_current=True, - missing_baseline=False, - ) - ) - else: - read_estimate(new_path, statistic=cast("Statistic", stat)) - continue - - if missing_current or missing_baseline: - gaps.append( - CoverageGap( - suite="exact", - group=group, - bench=bench, - baseline_bench=bench, - missing_current=missing_current, - missing_baseline=missing_baseline, - ) - ) - continue - - current = read_estimate(new_path, statistic=cast("Statistic", stat)) - baseline = read_estimate(base_path, statistic=cast("Statistic", stat)) - - comparisons.append( - Comparison( - suite="exact", - group=group, - bench=bench, - baseline=baseline, - current=current, - assessment=assess_change(baseline, current), - ) - ) - - if selected_groups and not any((criterion_dir / group).is_dir() for group in selected_groups): - gaps.append(_entire_suite_gap("exact")) - - return ComparisonCollection(comparisons=comparisons, gaps=gaps) - - -def _ordered_vs_linalg_comparison_benches( - group_dir: Path, - baseline_name: str, - *, - suite: str, - scope: str, -) -> list[str]: - """Return expected or discovered comparison rows in stable order.""" - dim = _dim_from_vs_linalg_group(group_dir.name) - if scope == "release-signal": - present = set(VS_LINALG_LA_STACK_BENCHES) - if dim is not None: - present.update(VS_LINALG_RELEASE_SIGNAL_BENCHES_BY_DIM.get(dim, [])) - else: - present = { - child.name - for child in group_dir.iterdir() - if child.is_dir() and ((child / "new" / "estimates.json").exists() or (child / baseline_name / "estimates.json").exists()) - } - - selected = {bench for bench in present if _is_selected_comparison_row(group_dir.name, bench, suite=suite, scope=scope)} - ordered = [bench for bench in VS_LINALG_BENCH_ORDER if bench in selected] - extras = sorted(selected.difference(VS_LINALG_BENCH_ORDER)) - return [*ordered, *extras] - - -def _vs_linalg_dimension_groups(criterion_dir: Path, scope: str) -> list[tuple[int, Path]]: - """Return canonical or discovered dimension groups for a comparison.""" - if scope == "release-signal": - return [(dim, criterion_dir / f"d{dim}") for dim in VS_LINALG_CANONICAL_DIMS] - - groups: list[tuple[int, Path]] = [] - for group_dir in criterion_dir.iterdir(): - if not group_dir.is_dir(): - continue - dim = _dim_from_vs_linalg_group(group_dir.name) - if dim is not None: - groups.append((dim, group_dir)) - return groups - - -def _read_optional_estimate(estimates_json: Path, stat: str) -> Estimate | None: - """Read an optional Criterion estimate.""" - if not estimates_json.exists(): - return None - return read_estimate(estimates_json, statistic=cast("Statistic", stat)) - - -def _baseline_peer_estimates( - group_dir: Path, - bench: str, - baseline_name: str, - stat: str, -) -> tuple[Estimate | None, Estimate | None]: - """Return last-release nalgebra/faer context for a la-stack vs_linalg bench.""" - peers = VS_LINALG_BASELINE_PEERS.get(bench) - if peers is None: - return (None, None) - - nalgebra_bench, faer_bench = peers - nalgebra = _read_optional_estimate(group_dir / nalgebra_bench / baseline_name / "estimates.json", stat) - faer = _read_optional_estimate(group_dir / faer_bench / baseline_name / "estimates.json", stat) - return (nalgebra, faer) - - -def _comparison_bench_label(comparison: Comparison) -> str: - """Return the display label for a comparison table row.""" - if comparison.baseline_bench is None: - return comparison.bench - return f"{comparison.bench} (vs {comparison.baseline_bench})" - - -def _collect_vs_linalg_comparisons( - criterion_dir: Path, - baseline_name: str, - stat: str, - suite: str, - policy: ComparisonPolicy, -) -> ComparisonCollection: - """Compare vs_linalg results while retaining one-sided rows.""" - comparisons: list[Comparison] = [] - gaps: list[CoverageGap] = [] - dim_groups = _vs_linalg_dimension_groups(criterion_dir, policy.scope) - - for _dim, group_dir in sorted(dim_groups, key=lambda item: item[0]): - expected_benches = _ordered_vs_linalg_comparison_benches( - group_dir, - baseline_name, - suite=suite, - scope=policy.scope, - ) - for bench in expected_benches: - new_path = group_dir / bench / "new" / "estimates.json" - base_path = group_dir / bench / baseline_name / "estimates.json" - missing_current = not new_path.exists() - missing_baseline = not base_path.exists() - if missing_current or missing_baseline: - gaps.append( - CoverageGap( - suite="vs_linalg", - group=group_dir.name, - bench=bench, - baseline_bench=bench, - missing_current=missing_current, - missing_baseline=missing_baseline, - ) - ) - continue - - current = read_estimate(new_path, statistic=cast("Statistic", stat)) - baseline = read_estimate(base_path, statistic=cast("Statistic", stat)) - baseline_nalgebra, baseline_faer = _baseline_peer_estimates(group_dir, bench, baseline_name, stat) - - comparisons.append( - Comparison( - suite="vs_linalg", - group=group_dir.name, - bench=bench, - baseline=baseline, - current=current, - assessment=assess_change(baseline, current), - baseline_nalgebra=baseline_nalgebra, - baseline_faer=baseline_faer, - ) - ) - - present_dim_groups = [group_dir for _, group_dir in dim_groups if group_dir.is_dir()] - if not present_dim_groups: - gaps.append(_entire_suite_gap("vs_linalg")) - - return ComparisonCollection(comparisons=comparisons, gaps=gaps) - - -def _entire_suite_gap(suite: str) -> CoverageGap: - """Return an explicit marker for a wholly absent selected suite.""" - return CoverageGap( - suite=suite, - group="(entire suite)", - bench="all selected rows", - baseline_bench="all selected rows", - missing_current=True, - missing_baseline=True, - ) - - -def _snapshot_coverage_errors( - criterion_dir: Path, - *, - sample: str, - suite: str, - scope: str, -) -> list[str]: - """Return deterministic coverage errors for a selected snapshot scope.""" - errors: list[str] = [] - selected_suites = ("exact", "vs_linalg") if suite == "all" else (suite,) - - if "exact" in selected_suites: - errors.extend(_exact_snapshot_coverage_errors(criterion_dir, sample=sample, scope=scope)) - - if "vs_linalg" in selected_suites: - errors.extend(_vs_linalg_snapshot_coverage_errors(criterion_dir, sample=sample, scope=scope)) - - return errors - - -def _exact_snapshot_coverage_errors(criterion_dir: Path, *, sample: str, scope: str) -> list[str]: - """Return exact-suite snapshot gaps.""" - if not any((criterion_dir / group).is_dir() for group in EXACT_GROUPS): - return ["exact: entire selected suite is absent"] - if scope != "release-signal": - return [] - return [ - f"exact: missing {group}/{bench}/{sample}/estimates.json" - for group, benches in EXACT_GROUPS.items() - for bench in benches - if not (criterion_dir / group / bench / sample / "estimates.json").is_file() - ] - - -def _vs_linalg_snapshot_coverage_errors(criterion_dir: Path, *, sample: str, scope: str) -> list[str]: - """Return vs_linalg snapshot gaps, including canonical dimensions.""" - discovered_dims = {dim for child in criterion_dir.iterdir() if child.is_dir() and (dim := _dim_from_vs_linalg_group(child.name)) is not None} - if not discovered_dims: - return ["vs_linalg: entire selected suite is absent"] - if scope != "release-signal": - return [] - - errors: list[str] = [] - for dim in VS_LINALG_CANONICAL_DIMS: - group_dir = criterion_dir / f"d{dim}" - if not group_dir.is_dir(): - errors.append(f"vs_linalg: missing canonical dimension d{dim}") - continue - expected = set(VS_LINALG_LA_STACK_BENCHES) - expected.update(VS_LINALG_RELEASE_SIGNAL_BENCHES_BY_DIM.get(dim, [])) - errors.extend( - f"vs_linalg: missing d{dim}/{bench}/{sample}/estimates.json" - for bench in VS_LINALG_BENCH_ORDER - if bench in expected and not (group_dir / bench / sample / "estimates.json").is_file() - ) - return errors - - -def _collect_comparisons( - criterion_dir: Path, - baseline_name: str, - stat: str, - suite: str = "all", - policy: ComparisonPolicy = _DEFAULT_COMPARISON_POLICY, -) -> ComparisonCollection: - """Compare current results and report every expected coverage gap.""" - comparisons: list[Comparison] = [] - gaps: list[CoverageGap] = [] - if suite in ("all", "exact"): - exact = _collect_exact_comparisons(criterion_dir, baseline_name, stat, suite, policy) - comparisons.extend(exact.comparisons) - gaps.extend(exact.gaps) - if suite in ("all", "vs_linalg"): - vs_linalg = _collect_vs_linalg_comparisons(criterion_dir, baseline_name, stat, suite, policy) - comparisons.extend(vs_linalg.comparisons) - gaps.extend(vs_linalg.gaps) - return ComparisonCollection(comparisons=comparisons, gaps=gaps) - - -# --------------------------------------------------------------------------- -# Formatting -# --------------------------------------------------------------------------- - - -def format_time(ns: float) -> str: - """Format nanoseconds into a human-readable string.""" - if ns < 1_000: - return f"{ns:.1f} ns" - if ns < 1_000_000: - return f"{ns / 1_000:.2f} µs" - return f"{ns / 1_000_000:.2f} ms" - - -def _format_pct(pct: float, assessment: ChangeAssessment) -> str: - """Format point-estimate change without implying statistical significance.""" - del assessment - return f"{pct:+.1f}%" - - -def _format_confidence_interval(estimate: Estimate) -> str: - """Format a Criterion confidence interval without inventing missing bounds.""" - if estimate.lower is None or estimate.upper is None: - return "unavailable" - return f"[{format_time(estimate.lower)}, {format_time(estimate.upper)}]" - - -def _format_estimate(estimate: Estimate) -> str: - """Format a point estimate together with its Criterion interval.""" - return f"{format_time(estimate.point)} {_format_confidence_interval(estimate)}" - - -def _assessment_label(assessment: ChangeAssessment) -> str: - """Return a concise, explicit confidence-interval assessment label.""" - labels: dict[ChangeAssessment, str] = { - "improvement": "faster point estimate; marginal CIs separated", - "regression": "slower point estimate; marginal CIs separated", - "inconclusive": "marginal CIs overlap", - "unknown": "marginal CI unavailable", - } - return labels[assessment] - - -class _GroupedItem(Protocol): - @property - def suite(self) -> str: ... - - @property - def group(self) -> str: ... - - -def _group_by_suite[T: _GroupedItem](items: list[T]) -> dict[str, list[T]]: - """Group results or comparisons by benchmark suite.""" - suites: dict[str, list[T]] = {} - for item in items: - suites.setdefault(item.suite, []).append(item) - return suites - - -def _group_by_group[T: _GroupedItem](items: list[T]) -> dict[str, list[T]]: - """Group results or comparisons by their Criterion group name.""" - groups: dict[str, list[T]] = {} - for item in items: - groups.setdefault(item.group, []).append(item) - return groups - - -def _suite_heading(suite: str) -> str: - """Turn an internal suite name into a readable heading.""" - if suite == "exact": - return "Exact arithmetic" - if suite == "vs_linalg": - return "vs_linalg" - return suite - - -def _group_heading(group: str) -> str: - """Turn a Criterion group name into a readable heading.""" - # exact_d3 -> "D=3", exact_random_corpus_d3 -> - # "Random corpus D=3", exact_near_singular_3x3 -> - # "Near-singular 3x3", exact_hilbert_4x4 -> "Hilbert 4x4", etc. - for prefix, label in ( - ("exact_random_corpus_d", "Random corpus D="), - ("rational_input_d", "Rational input D="), - ("exact_d", "D="), - ): - if group.startswith(prefix): - return f"{label}{group.removeprefix(prefix)}" - if group == "exact_near_singular_3x3": - return "Near-singular 3x3" - if group == "exact_large_entries_3x3": - return "Large entries 3x3" - if group.startswith("exact_hilbert_"): - return f"Hilbert {group.removeprefix('exact_hilbert_')}" - return group - - -def _group_heading_for_suite(suite: str, group: str) -> str: - """Turn a Criterion group name into a readable heading for its suite.""" - if suite == "vs_linalg": - dim = _dim_from_vs_linalg_group(group) - if dim is not None: - return f"D={dim}" - return _group_heading(group) - - -def _snapshot_tables(results: list[BenchResult], stat: str) -> str: - """Generate one Markdown table per suite for a single set of results.""" - stat_label = stat.capitalize() - sections: list[str] = [] - - for suite, suite_items in _group_by_suite(results).items(): - sections.append(f"## {_suite_heading(suite)}") - lines = [ - f"| Case | Benchmark | {stat_label} | Criterion CI |", - "|:-----|:----------|-------:|-------:|", - ] - for group, items in _group_by_group(suite_items).items(): - case = _group_heading_for_suite(suite, group) - for r in items: - ci_range = _format_confidence_interval(r.estimate) - lines.append(f"| {case} | {r.bench} | {format_time(r.estimate.point)} | {ci_range} |") - sections.append("\n".join(lines)) - - return "\n\n".join(sections) - - -def comparison_tables(comparisons: list[Comparison], baseline_name: str) -> str: - """Generate one Markdown comparison table per suite.""" - sections: list[str] = [] - - for suite, suite_items in _group_by_suite(comparisons).items(): - sections.append(f"## {_suite_heading(suite)}") - has_peer_context = any(item.baseline_nalgebra_ns is not None or item.baseline_faer_ns is not None for item in suite_items) - if has_peer_context: - header = ( - f"| Case | Benchmark | {baseline_name} (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | " - f"Point-estimate ratio | {baseline_name} nalgebra | {baseline_name} faer |" - ) - lines = [ - header, - "|:-----|:----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:|", - ] - else: - lines = [ - f"| Case | Benchmark | {baseline_name} (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio |", - "|:-----|:----------|-------:|-------:|-------:|:-----------|--------:|", - ] - for group, items in _group_by_group(suite_items).items(): - case = _group_heading_for_suite(suite, group) - for c in items: - cells = [ - case, - _comparison_bench_label(c), - _format_estimate(c.baseline), - _format_estimate(c.current), - _format_pct(c.pct_change, c.assessment), - _assessment_label(c.assessment), - f"{c.speedup:.2f}x", - ] - if has_peer_context: - cells.extend( - [ - _format_estimate(c.baseline_nalgebra) if c.baseline_nalgebra is not None else "—", - _format_estimate(c.baseline_faer) if c.baseline_faer is not None else "—", - ] - ) - lines.append(f"| {' | '.join(cells)} |") - sections.append("\n".join(lines)) - - return "\n\n".join(sections) - - -def _coverage_table(gaps: list[CoverageGap], baseline_name: str) -> str: - """Render missing current/baseline rows in deterministic collection order.""" - if not gaps: - return "" - - lines = [ - "## Incomplete Comparison Coverage", - "", - "These expected rows were not classified because one or both Criterion samples are missing.", - "", - "| Suite | Group | Benchmark | Missing sample(s) |", - "|:------|:------|:----------|:------------------|", - ] - for gap in gaps: - missing: list[str] = [] - if gap.missing_current: - missing.append("current (`new`)") - if gap.missing_baseline: - missing.append(f"baseline (`{baseline_name}`)") - bench_label = gap.bench - if gap.baseline_bench != gap.bench: - bench_label = f"{gap.bench} (baseline row: {gap.baseline_bench})" - lines.append(f"| {_suite_heading(gap.suite)} | {gap.group} | {bench_label} | {', '.join(missing)} |") - - return "\n".join(lines) - - -def _artifact_timing(estimate: Estimate) -> TimingEstimate: - """Convert a Criterion estimate into the stricter persisted timing model.""" - if estimate.lower is None or estimate.upper is None: - msg = "release-performance artifacts require complete Criterion confidence intervals" - raise ValueError(msg) - return TimingEstimate( - median_ns=estimate.point, - ci_lower_ns=estimate.lower, - ci_upper_ns=estimate.upper, - ) - - -def _criterion_timing(estimate: TimingEstimate) -> Estimate: - """Convert a validated persisted timing back into the report model.""" - return Estimate( - point=estimate.median_ns, - lower=estimate.ci_lower_ns, - upper=estimate.ci_upper_ns, - ) - - -def _comparison_artifact_rows( - comparisons: list[Comparison], - *, - scope: str, -) -> list[PerformanceRow]: - """Convert complete comparisons into deterministic schema rows.""" - return [ - PerformanceRow( - suite=comparison.suite, - scope=scope, - benchmark_id=f"{comparison.group}/{comparison.bench}", - group=comparison.group, - benchmark=comparison.bench, - baseline_benchmark=comparison.baseline_bench or comparison.bench, - coverage_status="comparable", - coverage_note="", - baseline=_artifact_timing(comparison.baseline), - current=_artifact_timing(comparison.current), - baseline_nalgebra=(None if comparison.baseline_nalgebra is None else _artifact_timing(comparison.baseline_nalgebra)), - baseline_faer=None if comparison.baseline_faer is None else _artifact_timing(comparison.baseline_faer), - ) - for comparison in comparisons - ] - - -def _unavailable_artifact_rows( # noqa: PLR0913 - criterion_dir: Path, - *, - baseline_name: str, - stat: str, - suite: str, - scope: str, - policy: ComparisonPolicy, -) -> list[PerformanceRow]: - """Retain correctness-excluded baseline rows as explicit current-only data.""" - compatibility = policy.baseline_api_compatibility - unavailable = _unavailable_baseline_rows(policy) - rows: list[PerformanceRow] = [] - for group, bench in sorted(unavailable): - if not _is_selected_comparison_row(group, bench, suite=suite, scope=scope): - continue - row_suite = _comparison_row_suite(group) - current_path = criterion_dir / group / bench / "new" / "estimates.json" - if not current_path.is_file(): - continue - current = read_estimate(current_path, statistic=cast("Statistic", stat)) - rows.append( - PerformanceRow( - suite=row_suite, - scope=scope, - benchmark_id=f"{group}/{bench}", - group=group, - benchmark=bench, - baseline_benchmark=bench, - coverage_status="current-only", - coverage_note=( - f"Baseline {baseline_name} has no correctness-compatible benchmark row under {compatibility or 'the selected compatibility policy'}." - ), - baseline=None, - current=_artifact_timing(current), - ) - ) - return rows - - -def _artifact_comparisons(rows: tuple[PerformanceRow, ...]) -> list[Comparison]: - """Reconstruct comparison rows solely from the retained CSV model.""" - comparisons: list[Comparison] = [] - for row in rows: - if row.coverage_status != "comparable": - continue - if row.baseline is None or row.current is None: - msg = "comparable artifact row lost a required timing" - raise AssertionError(msg) - baseline = _criterion_timing(row.baseline) - current = _criterion_timing(row.current) - comparisons.append( - Comparison( - suite=row.suite, - group=row.group, - bench=row.benchmark, - baseline=baseline, - current=current, - assessment=assess_change(baseline, current), - baseline_bench=(row.baseline_benchmark if row.baseline_benchmark != row.benchmark else None), - baseline_nalgebra=(None if row.baseline_nalgebra is None else _criterion_timing(row.baseline_nalgebra)), - baseline_faer=None if row.baseline_faer is None else _criterion_timing(row.baseline_faer), - ) - ) - return comparisons - - -def _artifact_coverage_table(rows: tuple[PerformanceRow, ...], baseline_name: str) -> str: - """Render every one-sided measurement and note from retained CSV fields.""" - one_sided = [row for row in rows if row.coverage_status != "comparable"] - if not one_sided: - return "" - lines = [ - "## Coverage Notes", - "", - "One-sided rows retain the available measurement but are excluded from point-estimate change and ratio calculations.", - "", - f"| Benchmark | Coverage | {baseline_name} (point + CI) | Latest (point + CI) | Note |", - "|:----------|:---------|-----------------------------:|--------------------:|:-----|", - ] - for row in one_sided: - baseline = "—" if row.baseline is None else _format_estimate(_criterion_timing(row.baseline)) - current = "—" if row.current is None else _format_estimate(_criterion_timing(row.current)) - lines.append(f"| {row.benchmark_id} | {row.coverage_status} | {baseline} | {current} | {row.coverage_note} |") - return "\n".join(lines) - - -def _artifact_tables(bundle: PerformanceBundle) -> str: - """Render comparison and coverage tables from one validated artifact bundle.""" - comparisons = _artifact_comparisons(bundle.sorted_rows) - sections: list[str] = [] - if comparisons: - sections.append(comparison_tables(comparisons, bundle.context.release.baseline)) - coverage = _artifact_coverage_table(bundle.sorted_rows, bundle.context.release.baseline) - if coverage: - sections.append(coverage) - if not sections: - msg = "release-performance artifact contains no renderable rows" - raise ValueError(msg) - return "\n\n".join(sections) - - -def _read_cargo_version(root: Path) -> str: - cargo_toml = root / "Cargo.toml" - if not cargo_toml.exists(): - return "unknown" - data = tomllib.loads(cargo_toml.read_text(encoding="utf-8")) - package = data.get("package") - if isinstance(package, dict): - version = package.get("version") - if isinstance(version, str): - return version - return "unknown" - - -def _get_git_info(root: Path) -> tuple[str, str]: - """Return (short_hash, branch_or_tag).""" - short_hash = "unknown" - branch = "unknown" - try: - result = run_command("git", ["--no-pager", "rev-parse", "--short", "HEAD"], cwd=root) - short_hash = result.stdout.strip() - except ( - ExecutableNotFoundError, - OSError, - subprocess.CalledProcessError, - subprocess.TimeoutExpired, - ): - pass - try: - result = run_command("git", ["--no-pager", "rev-parse", "--abbrev-ref", "HEAD"], cwd=root) - branch = result.stdout.strip() - except ( - ExecutableNotFoundError, - OSError, - subprocess.CalledProcessError, - subprocess.TimeoutExpired, - ): - pass - return short_hash, branch - - -def _get_git_source_date(root: Path) -> str: - """Return the reproducible source revision timestamp normalized to UTC.""" - try: - result = run_command("git", ["--no-pager", "show", "-s", "--format=%cI", "HEAD"], cwd=root) - except ( - ExecutableNotFoundError, - OSError, - subprocess.CalledProcessError, - subprocess.TimeoutExpired, - ): - return "unknown" - value = result.stdout.strip() - if not value: - return "unknown" - try: - parsed = datetime.fromisoformat(value) - except ValueError: - return "unknown" - if parsed.tzinfo is None or parsed.utcoffset() is None: - return "unknown" - return parsed.astimezone(UTC).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def _report_source(root: Path) -> ReportSource: - """Capture stable source metadata before report artifacts leave a worktree.""" - short_hash, branch = _get_git_info(root) - return ReportSource( - version=_read_cargo_version(root), - commit=short_hash, - ref=branch, - revision_timestamp=_get_git_source_date(root), - ) - - -def _generate_markdown( - root: Path, - table: str, - settings: ReportSettings, - *, - source: ReportSource | None = None, - coverage_from_artifacts: bool = False, -) -> str: - """Generate the complete benchmark report content.""" - if source is None: - version = _read_cargo_version(root) - short_hash, branch = _get_git_info(root) - source_date = _get_git_source_date(root) - else: - version = source.version - short_hash = source.commit - branch = source.ref - source_date = source.revision_timestamp - - lines = [ - "# Benchmark Performance", - "", - f"**la-stack** v{version.removeprefix('v')} · `{short_hash}` ({branch})", - f"**Source revision timestamp**: {source_date} (deterministic report metadata; not the benchmark measurement time)", - "**Benchmark measurement timestamp**: not recorded by Criterion; use the provenance below to identify the measured revisions and environment.", - f"**Statistic**: {settings.stat}", - f"**Suite**: {settings.suite}", - f"**Scope**: {settings.scope}", - "", - "## Benchmark Results", - "", - ] - - if settings.baseline_name is not None and settings.baseline_name != "": - lines.append(f"Comparison against baseline **{settings.baseline_name}**:") - lines.append("") - lines.append( - "Negative point-estimate change means the current point estimate is smaller; " - "a baseline/current point-estimate ratio above 1.00 has the same meaning." - ) - lines.extend( - [ - "The CI-relation column reports only whether the two marginal Criterion intervals overlap. These are not paired confidence intervals", - "for the change, so the report makes no statistical-significance or performance-improvement claim from interval separation.", - ] - ) - lines.append("") - if settings.harness_provenance is None: - lines.append( - "**Reproducibility provenance**: unavailable — these Criterion samples predate provenance capture or were produced outside the " - "publication pipeline. CPU, OS, rustc, commit, dependency lock, harness digest, Criterion configuration, and two-revision " - "fixture validation are unknown." - ) - else: - lines.extend( - _provenance_markdown( - settings.harness_provenance, - include_compatibility_rows=not coverage_from_artifacts, - ) - ) - else: - lines.append("Current performance snapshot (no baseline comparison).") - - lines.extend(["", table, ""]) - - lines.append(HOW_TO_UPDATE_SECTION.rstrip("\n")) - - return "\n".join(lines) + "\n" - - -def _provenance_markdown( - provenance: HarnessProvenance, - *, - include_compatibility_rows: bool = True, -) -> list[str]: - """Render validated provenance without implying facts absent from metadata.""" - if provenance.schema == 1: - return [ - ( - "**Harness provenance**: legacy shared current harness metadata from " - f"`.la-stack-benchmark-harness.json` (baseline source `{provenance.baseline}`, SHA-256 `{provenance.sha256}`). " - "CPU, OS, rustc, commits, Criterion configuration, and fixture-gate results were not recorded." - ) - ] - - if provenance.measurement is None or provenance.publication is None or provenance.criterion is None or provenance.validation is None: - msg = "schema-2 benchmark provenance invariant violated" - raise AssertionError(msg) - - measurement = provenance.measurement - publication = provenance.publication - criterion = provenance.criterion - validation = provenance.validation - if provenance.mode == "shared-current-harness": - correctness_gate = ( - "- Correctness gate: `just test-bench-inputs` passed against both the current and baseline revisions using the shared current fixture harness." - ) - else: - correctness_gate = ( - "- Correctness gate: `just test-bench-inputs` passed for both referenced source revisions " - "during publication; this gate validates benchmark inputs separately from the historical timing measurements." - ) - lines = ["### Reproducibility Provenance", ""] - if measurement["status"] == "recorded": - lines.extend( - [ - "**Measurement environment**: recorded for both samples under one shared current harness.", - "", - f"- CPU: `{measurement['cpu']}`", - f"- OS: `{measurement['os']}`", - f"- rustc: `{measurement['rustc']}`", - f"- Current commit: `{measurement['current_commit']}`", - f"- Current Git clean: `{str(measurement['current_git_clean']).lower()}`", - f"- Current source-state SHA-256: `{measurement['current_source_state_sha256']}`", - f"- Baseline commit: `{measurement['baseline_commit']}`", - f"- Baseline Git clean: `{str(measurement['baseline_git_clean']).lower()}`", - f"- Baseline source-state SHA-256: `{measurement['baseline_source_state_sha256']}`", - f"- Cargo.lock SHA-256: `{measurement['cargo_lock_sha256']}`", - f"- Benchmark harness SHA-256: `{measurement['harness_sha256']}`", - ] - ) - else: - lines.extend( - [ - f"**Measurement environment**: unavailable — {measurement['reason']}", - "", - "The publication environment below validates report generation, not the historical timing environment.", - ] - ) - - lines.extend( - [ - "", - "**Publication and validation environment**:", - "", - f"- Publication CPU: `{publication['cpu']}`", - f"- Publication OS: `{publication['os']}`", - f"- Publication rustc: `{publication['rustc']}`", - f"- Publication commit: `{publication['commit']}`", - f"- Publication Git clean: `{str(publication['git_clean']).lower()}`", - f"- Publication source-state SHA-256: `{publication['source_state_sha256']}`", - f"- Publication Cargo.lock SHA-256: `{publication['cargo_lock_sha256']}`", - f"- Publication harness SHA-256: `{publication['harness_sha256']}`", - f"- Criterion suite/scope: `{criterion.suite}` / `{criterion.scope}`", - f"- Criterion statistic/sample: `{criterion.statistic}` / `{criterion.sample}`", - f"- Criterion dependency version: `{criterion.criterion_version}`", - f"- Baseline command: `{' '.join(criterion.baseline_command)}`", - f"- Current command: `{' '.join(criterion.current_command)}`", - correctness_gate, - f"- Validated current revision: `{validation['current_commit']}` (Git clean: `{str(validation['current_git_clean']).lower()}`;", - f" source-state SHA-256: `{validation['current_source_state_sha256']}`)", - f"- Validated baseline revision: `{validation['baseline_commit']}` (Git clean: `{str(validation['baseline_git_clean']).lower()}`;", - f" source-state SHA-256: `{validation['baseline_source_state_sha256']}`)", - ] - ) - compatibility = validation.get("baseline_api_compatibility") - if isinstance(compatibility, str) and compatibility != "none": - lines.extend( - [ - f"- Baseline API compatibility: `{compatibility}` selects only source-compatible benchmark calls;", - " one-sided rows outside the baseline's correctness domain are identified by the retained CSV coverage status and note.", - ] - ) - if ( - include_compatibility_rows - and compatibility == _PRE_RATIONAL_INPUT_API_COMPATIBILITY - and criterion.suite in {"all", "exact"} - and validation.get("shared_harness_rational_inputs") is True - ): - lines.extend( - [ - "- Baseline-unavailable rows: `rational_input_d{2..8}/*` were not timed because the baseline", - " predates the exact rational-input API; current samples remain required, but no cross-release speedup is claimed.", - ] - ) - return lines - - -def _read_raw_harness_provenance(criterion_dir: Path) -> dict[str, object]: - """Read the already-validated harness provenance for durable embedding.""" - path = criterion_dir / ".la-stack-benchmark-harness.json" - try: - raw = json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exc: - msg = f"malformed benchmark harness provenance JSON in {path}: {exc}" - raise ValueError(msg) from exc - if not isinstance(raw, dict) or not all(isinstance(key, str) for key in raw): - msg = f"expected JSON object in {path}" - raise TypeError(msg) - return cast("dict[str, object]", raw) - - -def _release_artifact_bundle( - *, - root: Path, - criterion_dir: Path, - baseline_name: str, - settings: ReportSettings, - collection: ComparisonCollection, -) -> PerformanceBundle: - """Build the trusted report dataset after Criterion and coverage validation.""" - provenance = settings.harness_provenance - if provenance is None or provenance.schema != 2: - msg = "release-performance artifacts require complete schema-2 benchmark provenance" - raise ValueError(msg) - policy = _comparison_policy(settings.scope, provenance) - rows = _comparison_artifact_rows(collection.comparisons, scope=settings.scope) - rows.extend( - _unavailable_artifact_rows( - criterion_dir, - baseline_name=baseline_name, - stat=settings.stat, - suite=settings.suite, - scope=settings.scope, - policy=policy, - ) - ) - source = _report_source(root) - return PerformanceBundle( - context=ArtifactContext( - release=ReleasePair( - current=f"v{source.version.removeprefix('v')}", - baseline=baseline_name, - ), - statistic="median", - suite=settings.suite, - scope=settings.scope, - source=source, - benchmark_provenance=_read_raw_harness_provenance(criterion_dir), - ), - rows=tuple(rows), - ) - - -def render_release_artifacts(paths: ArtifactPaths) -> str: - """Reload, validate, and render a report without Criterion, Cargo, or Git.""" - return render_bundle(load_bundle(paths), paths) - - -def render_historical_assets(criterion: Path, pair: SharedReleasePair, suite: str, scope: str) -> bytes: - """Render native release assets without assigning local measurement provenance.""" - policy = ComparisonPolicy(scope=scope, shared_harness_rational_inputs=False) - collection = _collect_comparisons(criterion, pair.baseline, "median", suite, policy) - if collection.gaps or not collection.comparisons: - msg = "historical assets omit required la-stack report rows" - raise ValueError(msg) - return ( - f"# Published benchmark assets\n\n**la-stack** {pair.current}; baseline **{pair.baseline}**.\n\n" - "Each release retains its original harness. Historical host, toolchain, source fingerprints, and correctness gates are unknown.\n" - "These assets cannot be promoted as a common-harness run. Use `just performance-local` for controlled measurements.\n\n" - "Negative change means a smaller point estimate. Marginal interval separation is not a paired confidence interval or a significance claim.\n\n" - + comparison_tables(collection.comparisons, pair.baseline) - + "\n" - ).encode() - - -def render_bundle(bundle: PerformanceBundle, paths: ArtifactPaths) -> str: - """Render validated retained data before publishing any output.""" - context = bundle.context - selection = CriterionSelection( - suite=cast("BenchmarkSuite", context.suite), - scope=cast("ComparisonScope", context.scope), - statistic="median", - sample="new", - ) - harness_provenance = _parse_harness_provenance( - context.benchmark_provenance, - path=paths.provenance, - expected_baseline=context.release.baseline, - expected=selection, - ) - settings = ReportSettings( - baseline_name=context.release.baseline, - stat="median", - suite=cast("BenchmarkSuite", context.suite), - scope=cast("ComparisonScope", context.scope), - harness_provenance=harness_provenance, - ) - return _generate_markdown( - paths.csv.parent, - _artifact_tables(bundle), - settings, - source=context.source, - coverage_from_artifacts=True, - ) - - -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - - -def _parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Compare la-stack benchmark results across Criterion baselines.", - ) - parser.add_argument( - "baseline", - nargs="?", - default="last", - help="Baseline name to compare against (default: 'last').", - ) - parser.add_argument( - "--snapshot", - action="store_true", - help="Generate a current-performance snapshot instead of comparing against a baseline.", - ) - parser.add_argument( - "--stat", - default="median", - choices=["mean", "median"], - help="Statistic to compare (default: median).", - ) - parser.add_argument( - "--suite", - default="all", - choices=SUITE_CHOICES, - help="Benchmark suite to compare (default: all).", - ) - parser.add_argument( - "--scope", - default="release-signal", - choices=SCOPE_CHOICES, - help="Comparison scope: release-signal compares la-stack latest against last with baseline peer context; all-benches compares every present bench.", - ) - parser.add_argument( - "--criterion-dir", - default="target/criterion", - help="Criterion output directory (default: target/criterion).", - ) - parser.add_argument( - "--repo-root", - help="Source checkout used for report metadata (default: the checkout containing this script).", - ) - parser.add_argument( - "--output", - default="target/bench-reports/performance.md", - help="Output markdown file (default: target/bench-reports/performance.md).", - ) - parser.add_argument( - "--csv-output", - help="Write schema-versioned comparison CSV input; requires an adjacent, distinct --provenance-output.", - ) - parser.add_argument( - "--provenance-output", - help="Write an adjacent comparison JSON provenance sidecar; requires a distinct --csv-output.", - ) - return parser.parse_args(argv) - - -def _comparison_policy(scope: str, provenance: HarnessProvenance | None) -> ComparisonPolicy: - """Build comparison coverage policy from validated provenance.""" - if provenance is None: - return ComparisonPolicy(scope=scope) - if provenance.schema == 1: - return ComparisonPolicy(scope=scope, shared_harness_rational_inputs=False) - if provenance.validation is None: - return ComparisonPolicy(scope=scope) - compatibility = provenance.validation.get("baseline_api_compatibility") - shared_harness_rational_inputs = provenance.validation.get("shared_harness_rational_inputs") - return ComparisonPolicy( - scope=scope, - baseline_api_compatibility=compatibility if isinstance(compatibility, str) else None, - shared_harness_rational_inputs=(shared_harness_rational_inputs if isinstance(shared_harness_rational_inputs, bool) else True), - ) - - -def _run_bench_hint(suite: str) -> str: - if suite == "exact": - return "just bench-exact" - if suite == "vs_linalg": - return "just bench-vs-linalg-la-stack" - return "just bench-latest" - - -def _save_baseline_hint(suite: str, baseline: str) -> str: - if suite == "exact": - return f"just bench-save-baseline {baseline} exact" - if suite == "vs_linalg": - return f"just bench-save-baseline {baseline} vs_linalg" - return f"just bench-save-baseline {baseline}" - - -def _resolve_artifact_paths( - args: argparse.Namespace, - *, - root: Path, - output_path: Path, - stat: Statistic, -) -> ArtifactPaths | None: - """Resolve and validate the optional release-performance artifact pair.""" - if (args.csv_output is None) != (args.provenance_output is None): - msg = "--csv-output and --provenance-output must be provided together" - raise ValueError(msg) - if args.csv_output is None: - return None - if args.snapshot or stat != "median": - msg = "release-performance artifacts require a median baseline comparison" - raise ValueError(msg) - - try: - paths = ArtifactPaths( - csv=Path(args.csv_output) if Path(args.csv_output).is_absolute() else root / args.csv_output, - provenance=(Path(args.provenance_output) if Path(args.provenance_output).is_absolute() else root / args.provenance_output), - ) - ensure_distinct_paths( - { - "Markdown output": output_path, - "artifact CSV": paths.csv, - "artifact provenance": paths.provenance, - } - ) - except (OSError, ValueError) as err: - msg = f"Invalid release-performance artifact paths: {err}" - raise ValueError(msg) from err - return paths - - -def _write_and_render_artifacts( # noqa: PLR0913 - paths: ArtifactPaths, - *, - output_path: Path, - root: Path, - criterion_dir: Path, - settings: ReportSettings, - collection: ComparisonCollection | None, -) -> None: - """Publish artifacts and Markdown as one rollback-capable operation.""" - baseline_name = settings.baseline_name - if baseline_name is None or collection is None: - msg = "release-performance artifacts require a completed baseline comparison" - raise ValueError(msg) - bundle = _release_artifact_bundle( - root=root, - criterion_dir=criterion_dir, - baseline_name=baseline_name, - settings=settings, - collection=collection, - ) - outputs = bundle_outputs(paths, bundle) - ensure_distinct_paths({"report": output_path, "artifact CSV": paths.csv, "artifact provenance": paths.provenance}) - outputs[output_path] = render_bundle(bundle, paths).encode("utf-8") - replace_many(outputs) - - -def _write_text_atomic(path: Path, text: str) -> None: - """Publish a standalone report through the shared transaction.""" - replace_many({path: text.encode("utf-8")}) - - -def main(argv: list[str] | None = None) -> int: # noqa: C901, PLR0911, PLR0912, PLR0915 - """Generate a benchmark snapshot or comparison report from CLI arguments.""" - args = _parse_args(sys.argv[1:] if argv is None else argv) - stat = cast("Statistic", args.stat) - suite = cast("BenchmarkSuite", args.suite) - scope = cast("ComparisonScope", args.scope) - selection = CriterionSelection(suite=suite, scope=scope, statistic=stat, sample="new") - - root = Path(args.repo_root).resolve() if args.repo_root is not None else _repo_root() - criterion_dir = root / args.criterion_dir - output_path = Path(args.output) if Path(args.output).is_absolute() else root / args.output - - try: - artifact_paths = _resolve_artifact_paths(args, root=root, output_path=output_path, stat=stat) - except ValueError as err: - print(err, file=sys.stderr) - return 2 - - if not criterion_dir.is_dir(): - print( - f"No Criterion results found at {criterion_dir}.\nRun benchmarks first:\n {_run_bench_hint(args.suite)}\n", - file=sys.stderr, - ) - return 2 - - baseline_name = None if args.snapshot else args.baseline - harness_provenance: HarnessProvenance | None = None - collection: ComparisonCollection | None = None - - if baseline_name: - try: - harness_provenance = _read_harness_provenance( - criterion_dir, - expected_baseline=baseline_name, - expected=selection, - ) - except (OSError, TypeError, ValueError) as err: - print(f"Invalid benchmark harness provenance: {err}", file=sys.stderr) - return 2 - - try: - collection = _collect_comparisons( - criterion_dir, - baseline_name, - stat, - suite, - _comparison_policy(scope, harness_provenance), - ) - except (OSError, KeyError, TypeError, ValueError) as err: - print(f"Invalid Criterion estimate data: {err}", file=sys.stderr) - return 2 - if collection.gaps: - print( - f"Incomplete benchmark coverage: {len(collection.gaps)} required comparison row(s) are missing; report publication aborted.", - file=sys.stderr, - ) - print(_coverage_table(collection.gaps, baseline_name), file=sys.stderr) - if not collection.comparisons: - print( - f"No comparison data found for baseline '{baseline_name}'.\n" - f"Save a baseline first:\n {_save_baseline_hint(args.suite, baseline_name)}\n" - f"Then run benchmarks:\n {_run_bench_hint(args.suite)}\n", - file=sys.stderr, - ) - return 2 - if not collection.comparisons: - print( - f"No comparison data found for baseline '{baseline_name}'.\n" - f"Save a baseline first:\n {_save_baseline_hint(args.suite, baseline_name)}\n" - f"Then run benchmarks:\n {_run_bench_hint(args.suite)}\n", - file=sys.stderr, - ) - return 2 - try: - table = comparison_tables(collection.comparisons, baseline_name) - except ValueError as err: - print(f"Invalid Criterion comparison data: {err}", file=sys.stderr) - return 2 - else: - coverage_errors = _snapshot_coverage_errors( - criterion_dir, - sample="new", - suite=suite, - scope=scope, - ) - if coverage_errors: - print("Incomplete benchmark coverage; snapshot publication aborted:", file=sys.stderr) - for error in coverage_errors: - print(f" - {error}", file=sys.stderr) - return 2 - try: - results = _collect_results(criterion_dir, "new", stat, suite) - except (OSError, KeyError, TypeError, ValueError) as err: - print(f"Invalid Criterion estimate data: {err}", file=sys.stderr) - return 2 - if not results: - print( - f"No benchmark results found.\nRun benchmarks first:\n {_run_bench_hint(args.suite)}\n", - file=sys.stderr, - ) - return 2 - table = _snapshot_tables(results, stat) - - settings = ReportSettings( - baseline_name=baseline_name, - stat=stat, - suite=suite, - scope=scope, - harness_provenance=harness_provenance, - ) - if artifact_paths is not None: - try: - _write_and_render_artifacts( - artifact_paths, - output_path=output_path, - root=root, - criterion_dir=criterion_dir, - settings=settings, - collection=collection, - ) - except (ExceptionGroup, OSError, KeyError, TypeError, ValueError) as err: - print( - f"Invalid release-performance artifact data: {format_exception_diagnostics(err)}", - file=sys.stderr, - ) - return 2 - print(f"📊 Wrote {artifact_paths.csv} and {artifact_paths.provenance}") - print(f"📊 Wrote {output_path}") - else: - md = _generate_markdown(root, table, settings) - try: - _write_text_atomic(output_path, md) - except (ExceptionGroup, OSError, ValueError) as err: - print(f"Could not write benchmark report: {format_exception_diagnostics(err)}", file=sys.stderr) - return 2 - print(f"📊 Wrote {output_path}") - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/benchmark_contract.py b/scripts/benchmark_contract.py deleted file mode 100644 index 59be4af..0000000 --- a/scripts/benchmark_contract.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Stable content identity for release benchmark measurements.""" - -import hashlib -from pathlib import Path -from typing import Final - - -def find_project_root(start: Path | None = None) -> Path: - """Find the nearest Cargo manifest from the caller's working directory.""" - directory = (start or Path.cwd()).resolve() - if directory.is_file(): - directory = directory.parent - for candidate in (directory, *directory.parents): - if (candidate / "Cargo.toml").is_file(): - return candidate - msg = "Could not locate Cargo.toml to determine benchmark root" - raise FileNotFoundError(msg) - - -BENCHMARK_CONTRACT_DIRS: Final[tuple[str, ...]] = ("benches",) -BENCHMARK_CONTRACT_FILES: Final[tuple[str, ...]] = ( - ".config/nextest.toml", - "Cargo.toml", - "Cargo.lock", - "justfile", - "rust-toolchain.toml", - "tests/exact_bench_config.rs", -) - - -def benchmark_contract_files(checkout: Path) -> list[Path]: - """Return files that determine benchmark code, inputs, and dependencies.""" - files: list[Path] = [] - for relative in BENCHMARK_CONTRACT_FILES: - path = checkout / relative - if not path.is_file(): - msg = f"benchmark contract file is missing: {path}" - raise FileNotFoundError(msg) - files.append(path) - for relative in BENCHMARK_CONTRACT_DIRS: - directory = checkout / relative - if not directory.is_dir(): - msg = f"benchmark contract directory is missing: {directory}" - raise FileNotFoundError(msg) - files.extend(path for path in directory.rglob("*") if path.is_file()) - return sorted(files, key=lambda path: path.relative_to(checkout).as_posix()) - - -def benchmark_contract_digest(checkout: Path) -> str: - """Hash benchmark code, inputs, dependency resolution, and toolchain.""" - digest = hashlib.sha256() - for path in benchmark_contract_files(checkout): - relative = path.relative_to(checkout).as_posix().encode() - payload = path.read_bytes() - digest.update(len(relative).to_bytes(8, "big")) - digest.update(relative) - digest.update(len(payload).to_bytes(8, "big")) - digest.update(payload) - return digest.hexdigest() diff --git a/scripts/benchmark_summaries.py b/scripts/benchmark_summaries.py deleted file mode 100644 index 213e2d1..0000000 --- a/scripts/benchmark_summaries.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Read historical la-stack summary schemas without rewriting their bytes or hashes.""" - -import csv -import hashlib -import io -import json -from dataclasses import dataclass -from pathlib import Path -from typing import Literal, cast - -from research_repo_tools.criterion import Estimate - -from performance_artifacts import ArtifactPaths, TimingEstimate, load_bundle - -SCHEMA_VERSION = 1 -COLUMNS = ( - "phase", - "benchmark_id", - "sample_count", - "mean_ns", - "mean_ci_lower_ns", - "mean_ci_upper_ns", - "median_ns", - "median_ci_lower_ns", - "median_ci_upper_ns", - "confidence_level", -) - - -@dataclass(frozen=True, slots=True) -class Measurement: - """One independently identified baseline or current measurement.""" - - phase: Literal["baseline", "current"] - benchmark_id: str - mean: TimingEstimate - median: TimingEstimate - - def __post_init__(self) -> None: - """Require a phase and a portable, nonempty Criterion identity.""" - if self.phase not in {"baseline", "current"} or not self.benchmark_id.strip(): - msg = "measurement requires a phase and benchmark ID" - raise ValueError(msg) - - -def read_object(path: Path) -> dict[str, object]: - """Read the legacy JSON object without assigning new digest semantics.""" - value = json.loads(path.read_bytes()) - if not isinstance(value, dict): - raise TypeError(f"expected a legacy JSON object: {path}") - return cast("dict[str, object]", value) - - -def full_summary_paths(report: ArtifactPaths) -> ArtifactPaths: - """Keep the complete summary adjacent to the selected report inputs.""" - return ArtifactPaths( - csv=report.csv.with_suffix(".full.csv"), - provenance=report.csv.with_suffix(".full.provenance.json"), - ) - - -def report_input_paths(report: ArtifactPaths) -> dict[str, Path]: - """Reserve selected and complete inputs against publication-output aliases.""" - full = full_summary_paths(report) - return { - "artifact CSV": report.csv, - "artifact provenance": report.provenance, - "complete summary CSV": full.csv, - "complete summary provenance": full.provenance, - } - - -def _validate_rows(rows: list[Measurement] | tuple[Measurement, ...]) -> None: - if {row.phase for row in rows} != {"baseline", "current"}: - msg = "complete local summaries require both measurement phases" - raise ValueError(msg) - keys = {(row.phase, row.benchmark_id) for row in rows} - if len(keys) != len(rows): - msg = "duplicate phase/benchmark identity in local summaries" - raise ValueError(msg) - - -def _parse_rows(text: str) -> tuple[Measurement, ...]: - reader = csv.DictReader(io.StringIO(text, newline="")) - if reader.fieldnames != list(COLUMNS): - msg = "unsupported complete-summary CSV columns" - raise ValueError(msg) - rows: list[Measurement] = [] - for row in reader: - if None in row or any(value is None for value in row.values()): - msg = "malformed complete-summary CSV row" - raise ValueError(msg) - phase = row["phase"] - if phase not in {"baseline", "current"}: - raise ValueError(f"invalid measurement phase: {phase!r}") - if row["sample_count"] != "100" or row["confidence_level"] != "0.95": - msg = "local summaries require full release sampling and 95% intervals" - raise ValueError(msg) - estimates = [ - TimingEstimate(*(Estimate(float(row[field])).point for field in fields)) - for fields in ( - ("mean_ns", "mean_ci_lower_ns", "mean_ci_upper_ns"), - ("median_ns", "median_ci_lower_ns", "median_ci_upper_ns"), - ) - ] - rows.append(Measurement(phase, row["benchmark_id"], *estimates)) - _validate_rows(rows) - return tuple(rows) - - -def _validate_saved_summary(report: ArtifactPaths) -> None: - full = full_summary_paths(report) - payload = full.csv.read_bytes() - rows = _parse_rows(payload.decode("utf-8")) - provenance = read_object(full.provenance) - expected = { - "schema_version": SCHEMA_VERSION, - "columns": list(COLUMNS), - "sha256": hashlib.sha256(payload).hexdigest(), - "counts": {phase: sum(row.phase == phase for row in rows) for phase in ("baseline", "current")}, - } - measurements = provenance.pop("measurements", None) - if not isinstance(measurements, dict) or type(measurements.get("schema_version")) is not int or measurements != expected: - msg = "complete-summary digest, schema, or phase counts do not match" - raise ValueError(msg) - if provenance != read_object(report.provenance): - msg = "complete summaries and selected report have different provenance" - raise ValueError(msg) - load_bundle(report) - - -def resolve_report_paths(root: Path, report: ArtifactPaths) -> ArtifactPaths: - """Use committed summaries after clean, without hiding partial scratch artifacts.""" - default = root / "target/bench-reports/performance.csv" - if ( - report.csv.resolve() != default.resolve() - or report.provenance.resolve() != default.with_suffix(".provenance.json").resolve() - or any(path.exists() or path.is_symlink() for path in report_input_paths(report).values()) - ): - return report - directory = root / "docs/performance" - pointer = directory / "latest.json" - if not pointer.exists(): - return report - metadata = read_object(pointer) - relative = metadata.get("run") - if type(metadata.get("schema_version")) is not int or metadata.get("schema_version") != SCHEMA_VERSION or not isinstance(relative, str): - msg = "invalid persistent benchmark pointer" - raise ValueError(msg) - run = (directory / relative).resolve() - if not run.is_relative_to(directory.resolve()) or len(Path(relative).parts) != 2: - msg = "persistent benchmark pointer escapes its run directory" - raise ValueError(msg) - retained = ArtifactPaths(csv=run / "performance.csv", provenance=run / "performance.provenance.json") - _validate_saved_summary(retained) - return retained diff --git a/scripts/criterion_dim_plot.py b/scripts/criterion_dim_plot.py deleted file mode 100644 index be47218..0000000 --- a/scripts/criterion_dim_plot.py +++ /dev/null @@ -1,1380 +0,0 @@ -#!/usr/bin/env python3 -"""Aggregate Criterion benchmark results into a time-vs-dimension chart. - -Reads Criterion output under: - target/criterion/d{D}/{benchmark}/{new|base}/estimates.json - -And writes: - docs/assets/bench/vs_linalg_{metric}_{stat}.csv - docs/assets/bench/vs_linalg_{metric}_{stat}.svg - -This is intended to create a single, README-friendly plot comparing la-stack to other -Rust linear algebra crates across dimensions. -""" - -import argparse -import hashlib -import json -import platform -import re -import subprocess -import sys -import tempfile -import tomllib -from collections.abc import Mapping -from dataclasses import dataclass -from pathlib import Path -from typing import Final, TypeGuard, cast - -from research_repo_tools.complete_runs import run_identity -from research_repo_tools.criterion import Comparison, Estimate, Statistic, read_estimate -from research_repo_tools.evidence import serialize_evidence -from research_repo_tools.files import replace_many -from research_repo_tools.process import ExecutableNotFoundError, cpu_description, format_exception_diagnostics, run_command -from research_repo_tools.publication import MarkerPair, plan_outputs, publish_publication, replace_section - -from benchmark_contract import benchmark_contract_digest, find_project_root -from benchmark_summaries import report_input_paths, resolve_report_paths -from performance_artifacts import ArtifactPaths, PerformanceBundle, TimingEstimate, ensure_distinct_paths, load_bundle - - -@dataclass(frozen=True, slots=True) -class Metric: - """Criterion benchmark names and display title for one plotted metric.""" - - la_bench: str - na_bench: str - fa_bench: str - title: str - - -@dataclass(frozen=True, slots=True) -class PlotRequest: - """Validated inputs required to render a benchmark SVG.""" - - csv_path: Path - out_svg: Path - title: str - stat: str - dims: tuple[int, ...] - la_label: str - na_label: str - fa_label: str - log_y: bool - - -@dataclass(frozen=True, slots=True) -class PlotCliArgs: - """Validated command-line options for the plot generator.""" - - metric: str - stat: str - sample: str - criterion_dir: str - performance_csv: str - out: str | None - csv: str | None - log_y: bool - no_plot: bool - update_readme: bool - readme: str - allow_partial: bool - - -@dataclass(frozen=True, slots=True) -class _ValidatedPerformance: - """One retained bundle bound to the checkout used for publication.""" - - bundle: PerformanceBundle - validation: dict[str, object] - paths: ArtifactPaths - - -@dataclass(frozen=True, slots=True) -class Row: - """Validated timing estimates for one benchmark dimension.""" - - dim: int - la_time: float - la_lo: float - la_hi: float - na_time: float - na_lo: float - na_hi: float - fa_time: float - fa_lo: float - fa_hi: float - - def __post_init__(self) -> None: - """Reject invalid dimensions, timings, and confidence intervals.""" - if self.dim <= 0: - msg = f"dimension must be positive: {self.dim}" - raise ValueError(msg) - Estimate(self.la_time, self.la_lo, self.la_hi) - Estimate(self.na_time, self.na_lo, self.na_hi) - Estimate(self.fa_time, self.fa_lo, self.fa_hi) - - -class ReadmeBenchmarkLinkError(ValueError): - """Raised when canonical README benchmark artifact links are incomplete.""" - - -type ParsedObject = dict[str, object] - - -METRICS: Final[dict[str, Metric]] = { - "det_via_lu": Metric( - la_bench="la_stack_det_via_lu", - na_bench="nalgebra_det_via_lu", - fa_bench="faer_det_via_lu", - title="Determinant via LU (factor + det)", - ), - "lu": Metric( - la_bench="la_stack_lu", - na_bench="nalgebra_lu", - fa_bench="faer_lu", - title="LU factorization", - ), - "lu_solve": Metric( - la_bench="la_stack_lu_solve", - na_bench="nalgebra_lu_solve", - fa_bench="faer_lu_solve", - title="LU solve (factor + solve)", - ), - "solve_from_lu": Metric( - la_bench="la_stack_solve_from_lu", - na_bench="nalgebra_solve_from_lu", - fa_bench="faer_solve_from_lu", - title="Solve from precomputed LU", - ), - "det_from_lu": Metric( - la_bench="la_stack_det_from_lu", - na_bench="nalgebra_det_from_lu", - fa_bench="faer_det_from_lu", - title="Determinant from precomputed LU", - ), - "dot": Metric( - la_bench="la_stack_dot", - na_bench="nalgebra_dot", - fa_bench="faer_dot_native", - title="Vector dot product", - ), - # Different names between crates. - "norm2_sq": Metric( - la_bench="la_stack_norm2_sq", - na_bench="nalgebra_norm_squared", - fa_bench="faer_norm2_sq", - title="Vector squared 2-norm", - ), - "inf_norm": Metric( - la_bench="la_stack_inf_norm", - na_bench="nalgebra_inf_norm", - fa_bench="faer_inf_norm", - title="Matrix infinity norm (max abs row sum)", - ), -} - -CANONICAL_DIMS: Final[tuple[int, ...]] = (2, 3, 4, 5, 8, 16, 32, 64) -_DEFAULT_PERFORMANCE_CSV: Final[str] = "target/bench-reports/performance.csv" -_RELEASE_VERSION_PATTERN: Final[str] = r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?" -_PROVENANCE_HARNESS_FILES: Final[tuple[str, ...]] = ( - ".config/nextest.toml", - "Cargo.toml", - "Cargo.lock", - "rust-toolchain.toml", - "justfile", - "tests/exact_bench_config.rs", -) - - -def _repo_root() -> Path: - """Resolve the checkout from the caller's working tree, including wheel installs.""" - return find_project_root() - - -def _dim_from_group_dir(name: str) -> int | None: - match = re.fullmatch(r"d(\d+)", name) - if match is None: - return None - return int(match.group(1)) - - -def _is_parsed_object(value: object) -> TypeGuard[ParsedObject]: - """Return true when a parsed JSON/TOML value is an object with string keys.""" - return isinstance(value, dict) and all(isinstance(key, str) for key in value) - - -def _require_parsed_object(value: object, context: str) -> ParsedObject: - if not _is_parsed_object(value): - msg = f"expected object for {context}" - raise TypeError(msg) - return value - - -def _discover_dims(criterion_dir: Path) -> list[int]: - dims: list[int] = [] - for child in criterion_dir.iterdir(): - if not child.is_dir(): - continue - d = _dim_from_group_dir(child.name) - if d is None: - continue - dims.append(d) - return sorted(dims) - - -def _read_cargo_package_version(cargo_toml: Path) -> str | None: - if not cargo_toml.exists(): - return None - - data = _read_cargo_toml(cargo_toml) - package = data.get("package") - if _is_parsed_object(package): - version = package.get("version") - if isinstance(version, str): - return version - return None - - -def _read_cargo_dependency_versions(cargo_toml: Path, names: set[str]) -> dict[str, str]: - if not cargo_toml.exists(): - return {} - - data = _read_cargo_toml(cargo_toml) - versions: dict[str, str] = {} - for section in ("dependencies", "dev-dependencies", "build-dependencies"): - table = data.get(section) - if not _is_parsed_object(table): - continue - for name in names: - value = table.get(name) - if isinstance(value, str): - versions[name] = value - elif _is_parsed_object(value): - version = value.get("version") - if isinstance(version, str): - versions[name] = version - - return versions - - -def _read_cargo_toml(cargo_toml: Path) -> ParsedObject: - data: object = tomllib.loads(cargo_toml.read_text(encoding="utf-8")) - return _require_parsed_object(data, str(cargo_toml)) - - -def _detect_versions(root: Path) -> dict[str, str]: - cargo_toml = root / "Cargo.toml" - package_version = _read_cargo_package_version(cargo_toml) or "unknown" - dep_versions = _read_cargo_dependency_versions(cargo_toml, {"nalgebra", "faer"}) - dep_versions.update(_read_cargo_dependency_versions(root / "benches/comparison/Cargo.toml", {"nalgebra", "faer"})) - - return { - "la-stack": package_version, - "nalgebra": dep_versions.get("nalgebra", "unknown"), - "faer": dep_versions.get("faer", "unknown"), - } - - -def _print_versions(versions: dict[str, str]) -> None: - order = ["la-stack", "nalgebra", "faer"] - text = ", ".join(f"{name}={versions.get(name, 'unknown')}" for name in order) - print(f"Detected crate versions for legend: {text}", file=sys.stderr) - - -def _format_legend_label(name: str, version: str) -> str: - if version == "unknown": - return name - return f"{name} v{version}" - - -def _read_estimate(estimates_json: Path, stat: str) -> tuple[float, float, float]: - estimate = read_estimate(estimates_json, statistic=cast("Statistic", stat)) - if estimate.lower is None or estimate.upper is None: - raise ValueError(f"{estimates_json}: plotted estimates require a complete confidence interval") - return estimate.point, estimate.lower, estimate.upper - - -def _write_csv(out_csv: Path, rows: list[Row]) -> None: - out_csv.parent.mkdir(parents=True, exist_ok=True) - with out_csv.open("w", encoding="utf-8", newline="\n") as f: - f.write("D,la_stack,la_lo,la_hi,nalgebra,na_lo,na_hi,faer,fa_lo,fa_hi\n") - for row in rows: - f.write(f"{row.dim},{row.la_time},{row.la_lo},{row.la_hi},{row.na_time},{row.na_lo},{row.na_hi},{row.fa_time},{row.fa_lo},{row.fa_hi}\n") - - -def _pct_reduction(baseline: float, value: float) -> str: - """Percent time reduction relative to baseline (positive = value is faster).""" - pct = Comparison("dimension", Estimate(baseline), Estimate(value)).percent_reduction - return f"{pct:+.1f}%" - - -def _markdown_table(rows: list[Row], stat: str) -> str: - lines = [ - f"| D | la-stack {stat} (ns) | nalgebra {stat} (ns) | faer {stat} (ns) | reduction vs nalgebra (point est.) | reduction vs faer (point est.) |", - "|---:|--------------------:|--------------------:|----------------:|---------------------:|----------------:|", - ] - - for row in rows: - pct_vs_na = _pct_reduction(row.na_time, row.la_time) - pct_vs_fa = _pct_reduction(row.fa_time, row.la_time) - lines.append(f"| {row.dim} | {row.la_time:,.3f} | {row.na_time:,.3f} | {row.fa_time:,.3f} | {pct_vs_na} | {pct_vs_fa} |") - - return "\n".join(lines) - - -def _readme_table_markers(metric: str, stat: str, sample: str) -> tuple[str, str]: - tag = f"BENCH_TABLE:{metric}:{stat}:{sample}" - return (f"", f"") - - -def _update_readme_table(readme_path: Path, marker_begin: str, marker_end: str, table_md: str) -> bool: - original = readme_path.read_bytes() - updated = replace_section(original, MarkerPair(marker_begin, marker_end), table_md) - if updated == original: - return False - replace_many({readme_path: updated}) - return True - - -def _replace_readme_benchmark_asset_versions(text: str, *, metric: str, stat: str, version: str) -> str: - """Update the selected README benchmark links only after its artifacts exist.""" - if re.fullmatch(_RELEASE_VERSION_PATTERN, version) is None: - msg = f"Cargo package version is not valid for a release-pinned README link: {version!r}" - raise ReadmeBenchmarkLinkError(msg) - - asset_stem = f"/docs/assets/bench/vs_linalg_{metric}_{stat}" - expected_assets = sorted((f"{asset_stem}.csv", f"{asset_stem}.provenance.json", f"{asset_stem}.svg")) - pattern = re.compile( - r"(?Phttps://(?:github\.com/acgetchell/la-stack/(?:blob|raw|tree)/|raw\.githubusercontent\.com/acgetchell/la-stack/))" - rf"v(?P{_RELEASE_VERSION_PATTERN})" - rf"(?P{re.escape(asset_stem)}(?:\.csv|\.provenance\.json|\.svg))" - ) - matches = list(pattern.finditer(text)) - actual_assets = sorted(match.group("asset") for match in matches) - if actual_assets != expected_assets: - msg = f"README must contain exactly one tag-pinned link for each published benchmark asset; expected {expected_assets}, found {actual_assets}" - raise ReadmeBenchmarkLinkError(msg) - - return pattern.sub(lambda match: f"{match.group('prefix')}v{version}{match.group('asset')}", text) - - -def _gp_quote(s: str) -> str: - # gnuplot supports single-quoted strings; escape backslashes and single quotes. - return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'" - - -def _render_svg_with_gnuplot(req: PlotRequest) -> None: - req.out_svg.parent.mkdir(parents=True, exist_ok=True) - - xtics = ", ".join(str(d) for d in req.dims) - - gp_lines = [ - "set terminal svg size 960,540 noenhanced", - f"set output {_gp_quote(str(req.out_svg))}", - "set datafile separator comma", - "set grid", - "set key left top", - f"set title {_gp_quote(req.title)}", - "set xlabel 'Dimension D'", - f"set ylabel {_gp_quote(f'{req.stat} time (ns)')}", - f"set xtics ({xtics})", - "set style line 1 lc rgb '#1f77b4' lt 1 lw 2 pt 7 ps 1", - "set style line 2 lc rgb '#ff7f0e' lt 1 lw 2 pt 5 ps 1", - "set style line 3 lc rgb '#2ca02c' lt 1 lw 2 pt 9 ps 1", - "set style data linespoints", - "set tics nomirror", - "set border linewidth 1", - ] - - if req.log_y: - gp_lines.append("set logscale y 10") - - gp_lines.extend( - [ - "plot \\", - f" {_gp_quote(str(req.csv_path))} using 1:2:3:4 with yerrorlines ls 1 title {_gp_quote(req.la_label)}, \\", - f" {_gp_quote(str(req.csv_path))} using 1:5:6:7 with yerrorlines ls 2 title {_gp_quote(req.na_label)}, \\", - f" {_gp_quote(str(req.csv_path))} using 1:8:9:10 with yerrorlines ls 3 title {_gp_quote(req.fa_label)}", - ] - ) - - try: - run_command("gnuplot", [], input="\n".join(gp_lines)) - except ExecutableNotFoundError as exc: - msg = "gnuplot not found. Install it (macOS: `brew install gnuplot`) or re-run with --no-plot." - raise FileNotFoundError(msg) from exc - - -def _parse_args(argv: list[str]) -> PlotCliArgs: - parser = argparse.ArgumentParser(description="Plot Criterion time vs dimension for la-stack vs nalgebra/faer.") - - parser.add_argument( - "--metric", - default="lu_solve", - choices=sorted(METRICS.keys()), - help="Which vs_linalg metric to plot.", - ) - parser.add_argument( - "--stat", - default="median", - choices=["mean", "median"], - help="Statistic to plot from estimates.json.", - ) - parser.add_argument( - "--sample", - default="new", - choices=["new", "base"], - help="Which Criterion run directory to read (new = most recent).", - ) - parser.add_argument( - "--criterion-dir", - default="target/criterion", - help="Criterion output directory for exploratory rendering (default: target/criterion).", - ) - parser.add_argument( - "--performance-csv", - default=_DEFAULT_PERFORMANCE_CSV, - help=( - "Retained performance-release CSV used by --update-readme; the provenance JSON must be adjacent (default: target/bench-reports/performance.csv)." - ), - ) - parser.add_argument( - "--out", - default=None, - help="Output SVG path (default: docs/assets/bench/vs_linalg_{metric}_{stat}.svg).", - ) - parser.add_argument( - "--csv", - default=None, - help="Output CSV path (default: docs/assets/bench/vs_linalg_{metric}_{stat}.csv).", - ) - parser.add_argument( - "--log-y", - action="store_true", - help="Use a log-scale y-axis.", - ) - parser.add_argument( - "--no-plot", - action="store_true", - help="Only write CSV (skip gnuplot/SVG).", - ) - parser.add_argument( - "--update-readme", - action="store_true", - help="Update a Markdown table in README.md between BENCH_TABLE markers.", - ) - parser.add_argument( - "--readme", - default="README.md", - help="Path to README file to update (default: README.md at repo root).", - ) - parser.add_argument( - "--allow-partial", - action="store_true", - help="Allow incomplete dimensions for exploratory CSV/SVG output; incompatible with --update-readme.", - ) - - args = parser.parse_args(argv) - return PlotCliArgs( - metric=_required_str_attr(args, "metric"), - stat=_required_str_attr(args, "stat"), - sample=_required_str_attr(args, "sample"), - criterion_dir=_required_str_attr(args, "criterion_dir"), - performance_csv=_required_str_attr(args, "performance_csv"), - out=_optional_str_attr(args, "out"), - csv=_optional_str_attr(args, "csv"), - log_y=_required_bool_attr(args, "log_y"), - no_plot=_required_bool_attr(args, "no_plot"), - update_readme=_required_bool_attr(args, "update_readme"), - readme=_required_str_attr(args, "readme"), - allow_partial=_required_bool_attr(args, "allow_partial"), - ) - - -def _required_str_attr(args: argparse.Namespace, name: str) -> str: - value = getattr(args, name) - if not isinstance(value, str): - msg = f"argparse returned non-string value for {name}: {value!r}" - raise TypeError(msg) - return value - - -def _optional_str_attr(args: argparse.Namespace, name: str) -> str | None: - value = getattr(args, name) - if value is None or isinstance(value, str): - return value - msg = f"argparse returned non-string value for {name}: {value!r}" - raise TypeError(msg) - - -def _required_bool_attr(args: argparse.Namespace, name: str) -> bool: - value = getattr(args, name) - if not isinstance(value, bool): - msg = f"argparse returned non-bool value for {name}: {value!r}" - raise TypeError(msg) - return value - - -def _resolve_under_root(root: Path, arg: str) -> Path: - path = Path(arg) - return path if path.is_absolute() else root / path - - -def _resolve_output_paths(root: Path, metric: str, stat: str, out_svg: str | None, out_csv: str | None) -> tuple[Path, Path]: - svg = Path(out_svg) if out_svg is not None else Path(f"docs/assets/bench/vs_linalg_{metric}_{stat}.svg") - csv = Path(out_csv) if out_csv is not None else Path(f"docs/assets/bench/vs_linalg_{metric}_{stat}.csv") - - if not svg.is_absolute(): - svg = root / svg - if not csv.is_absolute(): - csv = root / csv - - return (svg, csv) - - -def _resolve_performance_paths(root: Path, performance_csv: str) -> ArtifactPaths: - """Resolve a retained performance CSV and its adjacent provenance JSON.""" - csv = _resolve_under_root(root, performance_csv) - return resolve_report_paths(root, ArtifactPaths(csv=csv, provenance=csv.with_suffix(".provenance.json"))) - - -def _collect_rows(criterion_dir: Path, dims: list[int], metric: Metric, stat: str, sample: str) -> tuple[list[Row], list[str]]: - rows: list[Row] = [] - skipped: list[str] = [] - - for d in dims: - group_dir = criterion_dir / f"d{d}" - la_est = group_dir / metric.la_bench / sample / "estimates.json" - na_est = group_dir / metric.na_bench / sample / "estimates.json" - fa_est = group_dir / metric.fa_bench / sample / "estimates.json" - - missing = [ - bench - for bench, path in ( - (metric.la_bench, la_est), - (metric.na_bench, na_est), - (metric.fa_bench, fa_est), - ) - if not path.exists() - ] - if missing: - skipped.append(f"d{d} (missing {', '.join(missing)})") - continue - - la, la_lo, la_hi = _read_estimate(la_est, stat) - na, na_lo, na_hi = _read_estimate(na_est, stat) - fa, fa_lo, fa_hi = _read_estimate(fa_est, stat) - rows.append( - Row( - dim=d, - la_time=la, - la_lo=la_lo, - la_hi=la_hi, - na_time=na, - na_lo=na_lo, - na_hi=na_hi, - fa_time=fa, - fa_lo=fa_lo, - fa_hi=fa_hi, - ) - ) - - return (rows, skipped) - - -def _git_value(root: Path, args: list[str]) -> str: - """Return deterministic Git provenance or an explicit unavailable label.""" - try: - value = run_command("git", ["--no-pager", *args], cwd=root).stdout.strip() - except ( - ExecutableNotFoundError, - OSError, - subprocess.CalledProcessError, - subprocess.TimeoutExpired, - ): - return "unavailable" - return value or "unavailable" - - -def _git_status_metadata(root: Path) -> tuple[bool | None, str]: - """Return checkout cleanliness and a deterministic digest of porcelain status.""" - try: - status = run_command( - "git", - ["--no-pager", "status", "--porcelain=v1", "--untracked-files=all"], - cwd=root, - ).stdout - except ( - ExecutableNotFoundError, - OSError, - subprocess.CalledProcessError, - subprocess.TimeoutExpired, - ): - return (None, hashlib.sha256(b"unavailable").hexdigest()) - return (not status.strip(), hashlib.sha256(status.encode()).hexdigest()) - - -def _source_state_digest(root: Path) -> tuple[str, bool]: - """Hash the measured library source so dirty runs remain identifiable.""" - source_dir = root / "src" - files = ( - sorted( - (path for path in source_dir.rglob("*") if path.is_file()), - key=lambda path: path.relative_to(root).as_posix(), - ) - if source_dir.is_dir() - else [] - ) - digest = hashlib.sha256() - if not files: - digest.update(b"MISSING:src/") - return (digest.hexdigest(), True) - for path in files: - relative = path.relative_to(root).as_posix().encode() - payload = path.read_bytes() - digest.update(len(relative).to_bytes(8, "big")) - digest.update(relative) - digest.update(len(payload).to_bytes(8, "big")) - digest.update(payload) - return (digest.hexdigest(), False) - - -def _provenance_harness_files(root: Path) -> tuple[list[Path], list[str]]: - """Return stable harness files and explicitly list any missing members.""" - files: list[Path] = [] - missing: list[str] = [] - for relative in _PROVENANCE_HARNESS_FILES: - path = root / relative - if path.is_file(): - files.append(path) - else: - missing.append(relative) - benches = root / "benches" - if benches.is_dir(): - files.extend(path for path in benches.rglob("*") if path.is_file()) - else: - missing.append("benches/") - return (sorted(set(files), key=lambda path: path.relative_to(root).as_posix()), sorted(missing)) - - -def _provenance_harness_digest(root: Path) -> tuple[str, list[str]]: - """Hash the benchmark harness while making missing inputs visible.""" - files, missing = _provenance_harness_files(root) - digest = hashlib.sha256() - for path in files: - relative = path.relative_to(root).as_posix().encode() - payload = path.read_bytes() - digest.update(len(relative).to_bytes(8, "big")) - digest.update(relative) - digest.update(len(payload).to_bytes(8, "big")) - digest.update(payload) - for relative in missing: - marker = f"MISSING:{relative}".encode() - digest.update(len(marker).to_bytes(8, "big")) - digest.update(marker) - return (digest.hexdigest(), missing) - - -def _rustc_version(root: Path) -> str: - """Return the rustc version used by the publication workflow.""" - try: - value = run_command( - "rustc", - ["--version"], - cwd=root, - timeout=60, - ).stdout.strip() - except ( - ExecutableNotFoundError, - OSError, - subprocess.CalledProcessError, - subprocess.TimeoutExpired, - ): - return "unavailable" - return value or "unavailable" - - -def _capture_environment(root: Path) -> dict[str, object]: - """Capture the current source, harness, toolchain, host, and Git state.""" - harness_sha256, missing_harness_files = _provenance_harness_digest(root) - cargo_lock = root / "Cargo.lock" - cargo_lock_sha256 = hashlib.sha256(cargo_lock.read_bytes()).hexdigest() if cargo_lock.is_file() else "unavailable" - cpu = cpu_description() - os_description = " ".join(part for part in (platform.system(), platform.release(), platform.machine()) if part).strip() or "unavailable" - git_clean, git_status_sha256 = _git_status_metadata(root) - source_state_sha256, source_missing = _source_state_digest(root) - return { - "cargo_lock_sha256": cargo_lock_sha256, - "commit": _git_value(root, ["--no-pager", "rev-parse", "HEAD"]), - "cpu": cpu, - "git_clean": git_clean, - "git_status_sha256": git_status_sha256, - "harness_sha256": harness_sha256, - "missing_harness_files": missing_harness_files, - "os": os_description, - "rustc": _rustc_version(root), - "source_missing": source_missing, - "source_state_sha256": source_state_sha256, - } - - -def _capture_exploratory_provenance(root: Path, *, args: PlotCliArgs, dims: list[int]) -> dict[str, object]: - """Capture provenance for rendering unverified local Criterion samples.""" - environment = _capture_environment(root) - criterion_version = _read_cargo_dependency_versions(root / "Cargo.toml", {"criterion"}).get("criterion", "unavailable") - return { - "artifact": "exploratory vs_linalg dimension plot", - "criterion": { - "benchmark_command": "unavailable", - "criterion_dependency": criterion_version, - "dimensions": dims, - "log_y": args.log_y, - "metric": args.metric, - "sample": args.sample, - "statistic": args.stat, - }, - "measurement": { - "reason": "the exploratory renderer did not run the benchmark command that produced these Criterion samples", - "status": "unavailable", - }, - "publication": { - **environment, - "correctness_gate": "not-run-exploratory", - }, - "schema": 1, - } - - -def _required_mapping(value: object, context: str) -> Mapping[str, object]: - """Parse one already-validated frozen provenance object.""" - if not isinstance(value, Mapping) or not all(isinstance(key, str) for key in value): - msg = f"retained performance provenance requires a {context} object" - raise TypeError(msg) - return cast("Mapping[str, object]", value) - - -def _json_compatible(value: object) -> object: - """Convert frozen retained-provenance containers into JSON containers.""" - if isinstance(value, Mapping): - return {str(key): _json_compatible(item) for key, item in value.items()} - if isinstance(value, tuple): - return [_json_compatible(item) for item in value] - return value - - -def _validate_performance_selection(root: Path, args: PlotCliArgs, bundle: PerformanceBundle) -> Mapping[str, object]: - """Validate the retained report selection and return recorded measurement metadata.""" - context = bundle.context - if context.suite not in {"all", "vs_linalg"}: - msg = f"retained performance data does not include vs_linalg: suite={context.suite!r}" - raise ValueError(msg) - if context.scope != "release-signal": - msg = f"README publication requires release-signal performance data, got {context.scope!r}" - raise ValueError(msg) - if context.statistic != args.stat: - msg = f"README statistic {args.stat!r} does not match retained performance statistic {context.statistic!r}" - raise ValueError(msg) - if context.release.current == context.release.baseline: - msg = "README publication requires a distinct release pair retained by `just performance-release`" - raise ValueError(msg) - - package_version = _read_cargo_package_version(root / "Cargo.toml") - if package_version is None: - msg = f"{root / 'Cargo.toml'} has no string package version" - raise ValueError(msg) - expected_release = f"v{package_version.removeprefix('v')}" - if context.release.current != expected_release: - msg = ( - f"retained performance data is for {context.release.current}, but Cargo.toml is {expected_release}; " - "run `just performance-release` for the current release" - ) - raise ValueError(msg) - - benchmark_provenance = context.benchmark_provenance - if benchmark_provenance.get("mode") != "shared-current-harness": - msg = "README publication requires locally recorded shared-current-harness performance data" - raise ValueError(msg) - measurement = _required_mapping(benchmark_provenance.get("measurement"), "measurement") - if measurement.get("status") != "recorded": - msg = "README publication requires recorded performance measurements" - raise ValueError(msg) - return measurement - - -def _validate_current_measurement(root: Path, measurement: Mapping[str, object]) -> dict[str, object]: - """Require retained measurements to match stable current checkout inputs.""" - cargo_lock = root / "Cargo.lock" - current_source_state, source_missing = _source_state_digest(root) - if source_missing: - msg = "current measured source directory is missing: src/" - raise ValueError(msg) - current = { - "cargo_lock_sha256": hashlib.sha256(cargo_lock.read_bytes()).hexdigest() if cargo_lock.is_file() else "unavailable", - "commit": _git_value(root, ["--no-pager", "rev-parse", "HEAD"]), - "source_state_sha256": current_source_state, - } - if current["commit"] == "unavailable": - msg = "current checkout commit 'unavailable'; cannot record README publication provenance" - raise ValueError(msg) - for retained_field, current_field in ( - ("cargo_lock_sha256", "cargo_lock_sha256"), - ("current_source_state_sha256", "source_state_sha256"), - ): - retained = measurement.get(retained_field) - current_value = current.get(current_field) - if retained != current_value: - msg = ( - f"retained performance {retained_field} {retained!r} does not match the current checkout {current_field} {current_value!r}; " - "run `just performance-release` again" - ) - raise ValueError(msg) - - retained_contract = measurement.get("benchmark_contract_sha256") - contract_status = "legacy-retained-artifact" - if retained_contract is None and measurement.get("current_commit") != current["commit"]: - msg = "legacy retained benchmark measurement does not match the current commit; run just performance-release to refresh it" - raise ValueError(msg) - if retained_contract is not None: - current_contract = benchmark_contract_digest(root) - if retained_contract != current_contract: - msg = ( - f"retained performance benchmark_contract_sha256 {retained_contract!r} does not match " - f"the current checkout benchmark contract {current_contract!r}; run `just performance-release` again" - ) - raise ValueError(msg) - contract_status = "matched" - return { - **current, - "benchmark_contract": contract_status, - } - - -def _validate_performance_bundle(root: Path, args: PlotCliArgs, bundle: PerformanceBundle) -> dict[str, object]: - """Bind retained release measurements to the current release checkout.""" - measurement = _validate_performance_selection(root, args, bundle) - return _validate_current_measurement(root, measurement) - - -def _row_from_estimates( - *, - dim: int, - la_stack: TimingEstimate, - nalgebra: TimingEstimate, - faer: TimingEstimate, -) -> Row: - return Row( - dim=dim, - la_time=la_stack.median_ns, - la_lo=la_stack.ci_lower_ns, - la_hi=la_stack.ci_upper_ns, - na_time=nalgebra.median_ns, - na_lo=nalgebra.ci_lower_ns, - na_hi=nalgebra.ci_upper_ns, - fa_time=faer.median_ns, - fa_lo=faer.ci_lower_ns, - fa_hi=faer.ci_upper_ns, - ) - - -def _collect_performance_rows(bundle: PerformanceBundle, metric: Metric) -> list[Row]: - """Extract current la-stack and retained same-harness peer timings.""" - rows_by_key = {(row.group, row.benchmark): row for row in bundle.rows if row.suite == "vs_linalg"} - rows: list[Row] = [] - for dim in CANONICAL_DIMS: - key = (f"d{dim}", metric.la_bench) - performance_row = rows_by_key.get(key) - if performance_row is None: - msg = f"retained performance data is missing {key[0]}/{key[1]}" - raise ValueError(msg) - missing = [ - name - for name, estimate in ( - ("current la-stack", performance_row.current), - ("nalgebra peer", performance_row.baseline_nalgebra), - ("faer peer", performance_row.baseline_faer), - ) - if estimate is None - ] - if missing: - msg = f"retained performance data for {key[0]}/{key[1]} is missing {', '.join(missing)} timing" - raise ValueError(msg) - if performance_row.current is None or performance_row.baseline_nalgebra is None or performance_row.baseline_faer is None: - msg = "retained performance timing presence invariant violated" - raise AssertionError(msg) - rows.append( - _row_from_estimates( - dim=dim, - la_stack=performance_row.current, - nalgebra=performance_row.baseline_nalgebra, - faer=performance_row.baseline_faer, - ) - ) - return rows - - -def _display_path(root: Path, path: Path) -> str: - try: - return path.resolve().relative_to(root.resolve()).as_posix() - except ValueError: - return str(path.resolve()) - - -def _capture_performance_provenance( - root: Path, - *, - args: PlotCliArgs, - dims: list[int], - retained: _ValidatedPerformance, -) -> dict[str, object]: - """Derive README artifact provenance from a validated performance bundle.""" - bundle = retained.bundle - paths = retained.paths - benchmark_provenance = bundle.context.benchmark_provenance - criterion = _required_mapping(benchmark_provenance.get("criterion"), "criterion") - measurement = _required_mapping(benchmark_provenance.get("measurement"), "measurement") - return { - "artifact": "README vs_linalg dimension plot", - "criterion": { - "baseline_command": _json_compatible(criterion.get("baseline_command")), - "criterion_dependency": criterion.get("criterion_version"), - "current_command": _json_compatible(criterion.get("current_command")), - "dimensions": dims, - "log_y": args.log_y, - "metric": args.metric, - "sample": args.sample, - "statistic": args.stat, - }, - "measurement": { - **cast("dict[str, object]", _json_compatible(measurement)), - "la_stack_sample": "current", - "peer_release_context": bundle.context.release.baseline, - "peer_sample": "baseline phase under shared current harness", - "source": "retained performance-release artifact", - }, - "performance_artifact": { - "csv": _display_path(root, paths.csv), - "csv_sha256": hashlib.sha256(paths.csv.read_bytes()).hexdigest(), - "provenance": _display_path(root, paths.provenance), - "provenance_sha256": hashlib.sha256(paths.provenance.read_bytes()).hexdigest(), - "release": { - "baseline": bundle.context.release.baseline, - "current": bundle.context.release.current, - }, - }, - "publication": { - **retained.validation, - "correctness_gate": "validated-performance-release-artifact", - }, - "schema": 2, - } - - -def _write_provenance(path: Path, provenance: dict[str, object]) -> None: - """Write stable, sorted JSON provenance beside generated benchmark assets.""" - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(provenance, indent=2, sort_keys=True) + "\n", encoding="utf-8", newline="\n") - - -def _validate_readme_target(root: Path, args: PlotCliArgs) -> int: # noqa: C901, PLR0911, PLR0912 - """Validate publication-only CLI invariants and README markers before rendering.""" - if not args.update_readme: - return 0 - if args.allow_partial: - print("--allow-partial is exploratory-only and cannot be combined with --update-readme", file=sys.stderr) - return 2 - if args.sample != "new": - print("README publication requires --sample new to identify the retained current-release measurements", file=sys.stderr) - return 2 - if args.stat != "median": - print("README publication requires --stat median to match performance-release artifacts", file=sys.stderr) - return 2 - if args.no_plot: - print("README publication requires SVG rendering; --no-plot is exploratory-only", file=sys.stderr) - return 2 - readme_path = _resolve_under_root(root, args.readme) - canonical_readme = (root / "README.md").resolve() - try: - readme_text = readme_path.read_text(encoding="utf-8") - except OSError as exc: - print(str(exc), file=sys.stderr) - return 2 - if readme_path.resolve() == canonical_readme: - expected_svg, expected_csv = _resolve_output_paths(root, args.metric, args.stat, None, None) - selected_svg, selected_csv = _resolve_output_paths(root, args.metric, args.stat, args.out, args.csv) - if selected_svg.resolve() != expected_svg.resolve() or selected_csv.resolve() != expected_csv.resolve(): - print( - f"README publication requires the canonical CSV/SVG destinations referenced by README.md; expected {expected_csv} and {expected_svg}", - file=sys.stderr, - ) - return 2 - expected_performance = _resolve_performance_paths(root, _DEFAULT_PERFORMANCE_CSV) - selected_performance = _resolve_performance_paths(root, args.performance_csv) - if selected_performance.csv.resolve() != expected_performance.csv.resolve(): - print( - f"README publication requires the canonical performance-release input {expected_performance.csv}; got {selected_performance.csv}", - file=sys.stderr, - ) - return 2 - try: - package_version = _read_cargo_package_version(root / "Cargo.toml") - if package_version is None: - msg = f"{root / 'Cargo.toml'} has no string package version" - raise ReadmeBenchmarkLinkError(msg) - _replace_readme_benchmark_asset_versions( - readme_text, - metric=args.metric, - stat=args.stat, - version=package_version, - ) - except (OSError, TypeError, ValueError, tomllib.TOMLDecodeError) as exc: - print(str(exc), file=sys.stderr) - return 2 - marker_begin, marker_end = _readme_table_markers(args.metric, args.stat, args.sample) - lines = readme_text.splitlines() - begin_count = sum(line.strip() == marker_begin for line in lines) - end_count = sum(line.strip() == marker_end for line in lines) - if begin_count != 1 or end_count != 1: - print(f"README markers not found or not unique (begin={begin_count}, end={end_count}).", file=sys.stderr) - return 2 - begin_idx = next(index for index, line in enumerate(lines) if line.strip() == marker_begin) - end_idx = next(index for index, line in enumerate(lines) if line.strip() == marker_end) - if begin_idx >= end_idx: - print("README markers are out of order.", file=sys.stderr) - return 2 - return 0 - - -def _validate_publication_paths(root: Path, args: PlotCliArgs, *, out_svg: Path, out_csv: Path) -> int: - """Reject input/output aliases before publication can begin.""" - paths = { - "CSV output": out_csv, - "provenance output": out_csv.with_suffix(".provenance.json"), - } - if not args.no_plot: - paths["SVG output"] = out_svg - if args.update_readme: - paths["README output"] = _resolve_under_root(root, args.readme) - performance = _resolve_performance_paths(root, args.performance_csv) - paths.update(report_input_paths(performance)) - try: - ensure_distinct_paths(paths) - except (OSError, ValueError) as exc: - print(f"Invalid benchmark publication paths: {exc}", file=sys.stderr) - return 2 - return 0 - - -def _publish_staged_files(pairs: list[tuple[Path, Path]]) -> bool: - """Publish the complete CSV/SVG/provenance/README group through the shared API.""" - try: - outputs = {destination: staged.read_bytes() for staged, destination in pairs} - replace_many({path: payload for path, payload in outputs.items() if path.is_symlink() or not path.is_file() or path.read_bytes() != payload}) - except (OSError, ValueError, ExceptionGroup) as exc: - print(f"could not publish benchmark artifacts: {format_exception_diagnostics(exc)}", file=sys.stderr) - return False - return True - - -def _update_staged_readme_publication( - *, - root: Path, - args: PlotCliArgs, - rows: list[Row], - staged_readme: Path, -) -> None: - """Stage the README table and canonical artifact links as one publication.""" - marker_begin, marker_end = _readme_table_markers(args.metric, args.stat, args.sample) - _update_readme_table(staged_readme, marker_begin, marker_end, _markdown_table(rows, args.stat)) - - readme_path = _resolve_under_root(root, args.readme) - if readme_path.resolve() != (root / "README.md").resolve(): - return - - package_version = _read_cargo_package_version(root / "Cargo.toml") - if package_version is None: - msg = f"{root / 'Cargo.toml'} has no string package version" - raise ReadmeBenchmarkLinkError(msg) - updated_readme = _replace_readme_benchmark_asset_versions( - staged_readme.read_bytes().decode("utf-8"), - metric=args.metric, - stat=args.stat, - version=package_version, - ) - staged_readme.write_bytes(updated_readme.encode("utf-8")) - - -def _stage_and_publish_outputs( # noqa: C901, PLR0913 - *, - root: Path, - args: PlotCliArgs, - rows: list[Row], - req: PlotRequest, - provenance: dict[str, object], - skipped: list[str], - validated_inputs: Mapping[Path, bytes] | None = None, -) -> int: - """Render every output in isolation, then replace publication files together.""" - final_provenance = req.csv_path.with_suffix(".provenance.json") - readme_path = _resolve_under_root(root, args.readme) - readme_original = readme_path.read_bytes() if args.update_readme else b"" - with tempfile.TemporaryDirectory(prefix=".criterion-dim-plot-", dir=root) as tmp: - stage_dir = Path(tmp) - staged_csv = stage_dir / "benchmark.csv" - staged_svg = stage_dir / "benchmark.svg" - staged_provenance = stage_dir / "benchmark.provenance.json" - _write_csv(staged_csv, rows) - _write_provenance(staged_provenance, provenance) - - pairs: list[tuple[Path, Path]] = [ - (staged_csv, req.csv_path), - (staged_provenance, final_provenance), - ] - if not args.no_plot: - staged_request = PlotRequest( - csv_path=staged_csv, - out_svg=staged_svg, - title=req.title, - stat=req.stat, - dims=req.dims, - la_label=req.la_label, - na_label=req.na_label, - fa_label=req.fa_label, - log_y=req.log_y, - ) - try: - _render_svg_with_gnuplot(staged_request) - except (FileNotFoundError, subprocess.CalledProcessError) as exc: - print(str(exc), file=sys.stderr) - print("No benchmark publication files were changed.", file=sys.stderr) - return 1 - pairs.append((staged_svg, req.out_svg)) - - if args.update_readme: - staged_readme = stage_dir / "README.md" - staged_readme.write_bytes(readme_original) - try: - _update_staged_readme_publication(root=root, args=args, rows=rows, staged_readme=staged_readme) - except (OSError, ValueError) as exc: - print(str(exc), file=sys.stderr) - print("No benchmark publication files were changed.", file=sys.stderr) - return 2 - pairs.append((staged_readme, readme_path)) - - if validated_inputs is None: - if not _publish_staged_files(pairs): - return 2 - else: - outputs = {destination.relative_to(root).as_posix(): staged.read_bytes() for staged, destination in pairs} - ensure_distinct_paths( - {**{f"input {path}": path for path in validated_inputs}, **{f"output {destination}": destination for _, destination in pairs}} - ) - inputs = {path.relative_to(root).as_posix(): data for path, data in validated_inputs.items()} - plan = plan_outputs(root, outputs, inputs=inputs) - if args.update_readme and dict(plan.originals)[readme_path.relative_to(root).as_posix()] != readme_original: - msg = "README publication inputs changed during rendering" - raise ValueError(msg) - publish_publication(plan) - - if skipped: - print("Warning: some dimension groups were skipped:") - for item in skipped: - print(f" - {item}") - print(f"Wrote CSV: {req.csv_path}") - if not args.no_plot: - print(f"Wrote SVG: {req.out_svg}") - print(f"Wrote provenance: {final_provenance}") - if args.update_readme: - print(f"Updated README benchmark publication: {_resolve_under_root(root, args.readme)}") - return 0 - - -def _complete_readme(root: Path, args: PlotCliArgs) -> int: - """Render the established figure format directly from shared complete runs.""" - # The report registry imports this module's metric definitions. - import performance_runs as complete # noqa: PLC0415 - - evidence = complete.retained_run(root, root / "target/bench-reports/performance") - run = complete.validate_scientific_run(evidence, release=True) - complete.verify_current_source(root, evidence) - values = complete.estimates_by_series(run, args.stat) - metric = METRICS[args.metric] - rows = [] - for dimension in CANONICAL_DIMS: - name = f"d{dimension}/{metric.la_bench}" - estimates = [values[series][name] for series in ("la-stack current", "nalgebra", "faer")] - numbers = [value for estimate in estimates for value in (estimate.point, estimate.lower, estimate.upper)] - if any(value is None for value in numbers): - msg = "README requires complete confidence intervals" - raise ValueError(msg) - rows.append(Row(dimension, *(float(value) for value in numbers if value is not None))) - sources = dict(evidence.sources) - context = dict(sources["current"].context) - baseline_context = dict(sources["baseline"].context) - report_config = root / complete.REPORT_CONFIG - inputs = {report_config: report_config.read_bytes()} - for field in ("source-inventory", "harness-inventory"): - inputs.update({root / name: (root / name).read_bytes() for name in json.loads(context[field])}) - payload, manifest = complete.scratch_paths(root / "target/bench-reports/performance") - identity = run_identity(evidence) - if not payload.exists(): - archive = root / complete.archive_directory(root) - payload = archive / "runs" / identity / "run.json" - manifest = payload.with_name("evidence.json") - for name in ("index.json", "latest.json", f"runs/{identity}/report.md"): - path = archive / name - inputs[path] = path.read_bytes() - inputs.update(zip((payload, manifest), serialize_evidence(evidence), strict=True)) - provenance: dict[str, object] = { - "schema": 3, - "artifact": "README vs_linalg dimension plot", - "run_id": identity, - "criterion": {"statistic": args.stat, "metric": args.metric, "dimensions": list(CANONICAL_DIMS), "log_y": args.log_y}, - "measurement": {"la_stack_phase": "current", "peer_phase": "baseline", "peer_release_context": baseline_context["release"]}, - "sources": { - phase: {"revision": source.revision, "source_sha256": source.source_sha256, "harness_sha256": source.harness_sha256} - for phase, source in evidence.sources - }, - } - out_svg, out_csv = _resolve_output_paths(root, args.metric, args.stat, args.out, args.csv) - request = PlotRequest( - csv_path=out_csv, - out_svg=out_svg, - title=f"{metric.title}: {args.stat} time vs dimension", - stat=args.stat, - dims=CANONICAL_DIMS, - la_label=_format_legend_label("la-stack", context["release"].removeprefix("v")), - na_label=_format_legend_label("nalgebra", baseline_context["dependency.nalgebra"]), - fa_label=_format_legend_label("faer", baseline_context["dependency.faer"]), - log_y=args.log_y, - ) - return _stage_and_publish_outputs(root=root, args=args, rows=rows, req=request, provenance=provenance, skipped=[], validated_inputs=inputs) - - -def main(argv: list[str] | None = None) -> int: # noqa: C901, PLR0911, PLR0912, PLR0915 - """Generate benchmark CSV and optional SVG or README output.""" - args = _parse_args(sys.argv[1:] if argv is None else argv) - - root = _repo_root() - - try: - rc = _validate_readme_target(root, args) - if rc != 0: - return rc - out_svg, out_csv = _resolve_output_paths(root, args.metric, args.stat, args.out, args.csv) - rc = _validate_publication_paths(root, args, out_svg=out_svg, out_csv=out_csv) - except (OSError, TypeError, ValueError) as exc: - print(f"Invalid retained benchmark inputs: {exc}", file=sys.stderr) - return 2 - if rc != 0: - return rc - if args.update_readme and _resolve_under_root(root, args.performance_csv).resolve() == (root / _DEFAULT_PERFORMANCE_CSV).resolve(): - try: - # The report registry imports this module's metric definitions. - import performance_runs as complete # noqa: PLC0415 - - candidates = list(complete.scratch_paths(root / "target/bench-reports/performance")) - if (root / complete.REPORT_CONFIG).exists(): - candidates.append(root / complete.archive_directory(root)) - if any(path.exists() or path.is_symlink() for path in candidates): - return _complete_readme(root, args) - except (OSError, ValueError, TypeError, KeyError, ExceptionGroup) as error: - print(f"Invalid complete performance run: {format_exception_diagnostics(error)}", file=sys.stderr) - return 2 - versions = _detect_versions(root) - _print_versions(versions) - - la_label = _format_legend_label("la-stack", versions.get("la-stack", "unknown")) - na_label = _format_legend_label("nalgebra", versions.get("nalgebra", "unknown")) - fa_label = _format_legend_label("faer", versions.get("faer", "unknown")) - - metric = METRICS[args.metric] - if args.update_readme: - try: - performance_paths = _resolve_performance_paths(root, args.performance_csv) - performance_bundle = load_bundle(performance_paths) - validation = _validate_performance_bundle(root, args, performance_bundle) - rows = _collect_performance_rows(performance_bundle, metric) - retained = _ValidatedPerformance(bundle=performance_bundle, validation=validation, paths=performance_paths) - provenance = _capture_performance_provenance( - root, - args=args, - dims=[row.dim for row in rows], - retained=retained, - ) - except (FileNotFoundError, OSError, TypeError, ValueError, AssertionError, tomllib.TOMLDecodeError) as exc: - print(f"Invalid retained performance-release data: {exc}", file=sys.stderr) - return 2 - skipped: list[str] = [] - else: - criterion_dir = _resolve_under_root(root, args.criterion_dir) - discovered_dims = _discover_dims(criterion_dir) if criterion_dir.exists() else [] - dims = discovered_dims if args.allow_partial else list(CANONICAL_DIMS) - if not args.allow_partial and not discovered_dims: - dims = [] - if not dims: - print( - f"No Criterion results found under {criterion_dir}.\n\nRun benchmarks first, e.g.:\n just bench-vs-linalg\n", - file=sys.stderr, - ) - return 2 - try: - rows, skipped = _collect_rows(criterion_dir, dims, metric, args.stat, args.sample) - except (OSError, KeyError, TypeError, ValueError) as exc: - print(f"Invalid Criterion estimate data: {exc}", file=sys.stderr) - return 2 - if not rows: - print( - "No benchmark results found to plot for the selected metric/stat.\n" - f"Expected files like:\n {criterion_dir}/d32/{metric.la_bench}/{args.sample}/estimates.json\n", - file=sys.stderr, - ) - if skipped: - print("Skipped groups:", *skipped, sep="\n - ", file=sys.stderr) - return 2 - if not args.allow_partial and skipped: - print( - "Canonical benchmark coverage is incomplete; no CSV, SVG, provenance, or README file was written.", - file=sys.stderr, - ) - print("Required dimensions: " + ", ".join(f"D={dim}" for dim in CANONICAL_DIMS), file=sys.stderr) - print("Coverage gaps:", *skipped, sep="\n - ", file=sys.stderr) - return 2 - provenance = _capture_exploratory_provenance(root, args=args, dims=[row.dim for row in rows]) - - dims_present = [row.dim for row in rows] - if not args.update_readme: - publication = provenance.get("publication") - if not isinstance(publication, dict): - msg = "publication provenance invariant violated" - raise TypeError(msg) - missing_harness_files = publication.get("missing_harness_files") - if not isinstance(missing_harness_files, list) or not all(isinstance(path, str) for path in missing_harness_files): - msg = "publication missing_harness_files invariant violated" - raise AssertionError(msg) - - title = f"{metric.title}: {args.stat} time vs dimension" - req = PlotRequest( - csv_path=out_csv, - out_svg=out_svg, - title=title, - stat=args.stat, - dims=tuple(dims_present), - la_label=la_label, - na_label=na_label, - fa_label=fa_label, - log_y=args.log_y, - ) - - return _stage_and_publish_outputs( - root=root, - args=args, - rows=rows, - req=req, - provenance=provenance, - skipped=skipped, - ) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/performance_artifacts.py b/scripts/performance_artifacts.py deleted file mode 100644 index f3618ba..0000000 --- a/scripts/performance_artifacts.py +++ /dev/null @@ -1,929 +0,0 @@ -"""Schema-versioned performance-comparison CSV and provenance artifacts.""" - -import csv -import hashlib -import io -import json -import math -import re -from collections.abc import Mapping -from dataclasses import dataclass -from types import MappingProxyType -from typing import TYPE_CHECKING, Literal, cast - -from research_repo_tools.criterion import Estimate -from research_repo_tools.evidence import verify_sha256 -from research_repo_tools.files import replace_many - -if TYPE_CHECKING: - from pathlib import Path - -SCHEMA_VERSION = 1 -SUITES = ("all", "exact", "vs_linalg") -SCOPES = ("release-signal", "all-benches") -COVERAGE_STATES = ("comparable", "current-only", "baseline-only") - -type CoverageState = Literal["comparable", "current-only", "baseline-only"] -type ReleaseApiCapability = Literal["pre-rational-input", "rational-input", "unknown"] -type RationalInputCoverage = Literal["excluded", "current-only", "comparable"] - -PRE_RATIONAL_INPUT_API_COMPATIBILITY = "la_stack_pre_rational_input_api" -NO_API_COMPATIBILITY = "none" - -_SEMVER_CORE_RE = re.compile( - r"^v?(?P0|[1-9][0-9]*)\.(?P0|[1-9][0-9]*)\.(?P0|[1-9][0-9]*)" - r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$" -) - - -@dataclass(frozen=True, slots=True) -class SharedHarnessCompatibility: - """API capabilities and row coverage for one shared-harness comparison.""" - - current: ReleaseApiCapability - baseline: ReleaseApiCapability - shared_harness_rational_inputs: bool - baseline_api_compatibility: str - rational_input_coverage: RationalInputCoverage - - -def _release_api_capability(release: str) -> ReleaseApiCapability: - """Classify a release by the public APIs used by the current harness.""" - match = _SEMVER_CORE_RE.fullmatch(release.strip()) - if match is None: - return "unknown" - version = tuple(int(match.group(part)) for part in ("major", "minor", "patch")) - if version <= (0, 4, 3): - msg = f"unsupported performance comparison release {release!r}; comparisons require v0.4.4 or newer (v0.4.3 and older are excluded)" - raise ValueError(msg) - if version <= (0, 4, 5): - return "pre-rational-input" - return "rational-input" - - -def resolve_shared_harness_compatibility( - *, - current: str, - baseline: str, - shared_harness_rational_inputs: bool, -) -> SharedHarnessCompatibility: - """Resolve adapters and rational-row coverage from both releases and the installed harness.""" - current_capability = _release_api_capability(current) - baseline_capability = _release_api_capability(baseline) - baseline_adapter = { - "pre-rational-input": PRE_RATIONAL_INPUT_API_COMPATIBILITY, - "rational-input": NO_API_COMPATIBILITY, - "unknown": NO_API_COMPATIBILITY, - }[baseline_capability] - if not shared_harness_rational_inputs: - rational_coverage: RationalInputCoverage = "excluded" - elif baseline_capability == "pre-rational-input": - rational_coverage = "current-only" - else: - rational_coverage = "comparable" - return SharedHarnessCompatibility( - current=current_capability, - baseline=baseline_capability, - shared_harness_rational_inputs=shared_harness_rational_inputs, - baseline_api_compatibility=baseline_adapter, - rational_input_coverage=rational_coverage, - ) - - -CSV_COLUMNS = ( - "schema_version", - "suite", - "scope", - "benchmark_id", - "group", - "benchmark", - "baseline_benchmark", - "coverage_status", - "coverage_note", - "baseline_median_ns", - "baseline_ci_lower_ns", - "baseline_ci_upper_ns", - "current_median_ns", - "current_ci_lower_ns", - "current_ci_upper_ns", - "baseline_nalgebra_median_ns", - "baseline_nalgebra_ci_lower_ns", - "baseline_nalgebra_ci_upper_ns", - "baseline_faer_median_ns", - "baseline_faer_ci_lower_ns", - "baseline_faer_ci_upper_ns", -) - - -@dataclass(frozen=True, slots=True) -class TimingEstimate: - """A finite positive median and its complete confidence interval.""" - - median_ns: float - ci_lower_ns: float - ci_upper_ns: float - - def __post_init__(self) -> None: - """Reject non-finite, non-positive, or reversed timing intervals.""" - if self.ci_lower_ns is None or self.ci_upper_ns is None: - msg = "retained timings require a complete confidence interval" - raise ValueError(msg) - Estimate(self.median_ns, self.ci_lower_ns, self.ci_upper_ns) - - -@dataclass(frozen=True, slots=True) -class PerformanceRow: - """One validated row in the release-performance reporting dataset.""" - - suite: str - scope: str - benchmark_id: str - group: str - benchmark: str - baseline_benchmark: str - coverage_status: CoverageState - coverage_note: str - baseline: TimingEstimate | None - current: TimingEstimate | None - baseline_nalgebra: TimingEstimate | None = None - baseline_faer: TimingEstimate | None = None - - def __post_init__(self) -> None: # noqa: C901 - """Preserve identity, coverage, and optional-timing invariants.""" - if self.suite not in SUITES: - msg = f"unsupported suite: {self.suite!r}" - raise ValueError(msg) - if self.scope not in SCOPES: - msg = f"unsupported scope: {self.scope!r}" - raise ValueError(msg) - for field, value in ( - ("benchmark_id", self.benchmark_id), - ("group", self.group), - ("benchmark", self.benchmark), - ("baseline_benchmark", self.baseline_benchmark), - ): - if not value.strip(): - msg = f"{field} must not be empty" - raise ValueError(msg) - expected_id = f"{self.group}/{self.benchmark}" - if self.benchmark_id != expected_id: - msg = f"benchmark_id must be {expected_id!r}, got {self.benchmark_id!r}" - raise ValueError(msg) - if self.coverage_status not in COVERAGE_STATES: - msg = f"unsupported coverage status: {self.coverage_status!r}" - raise ValueError(msg) - - expected_presence = { - "comparable": (True, True), - "current-only": (False, True), - "baseline-only": (True, False), - }[self.coverage_status] - observed_presence = (self.baseline is not None, self.current is not None) - if observed_presence != expected_presence: - msg = f"coverage status {self.coverage_status!r} requires baseline/current presence {expected_presence}, got {observed_presence}" - raise ValueError(msg) - if self.coverage_status == "comparable" and self.coverage_note: - msg = "comparable rows must not contain a coverage note" - raise ValueError(msg) - if self.coverage_status != "comparable" and not self.coverage_note.strip(): - msg = f"{self.coverage_status} rows require a coverage note" - raise ValueError(msg) - if (self.baseline_nalgebra is not None or self.baseline_faer is not None) and self.baseline is None: - msg = "baseline peer timings require a baseline la-stack timing" - raise ValueError(msg) - - -@dataclass(frozen=True, slots=True) -class ReleasePair: - """The current and baseline package identifiers represented by a report.""" - - current: str - baseline: str - - def __post_init__(self) -> None: - """Require two non-empty identifiers; local comparisons may match.""" - if not self.current.strip() or not self.baseline.strip(): - msg = "current and baseline releases must not be empty" - raise ValueError(msg) - - -@dataclass(frozen=True, slots=True) -class ReportSource: - """Stable source metadata needed to reproduce the report header.""" - - version: str - commit: str - ref: str - revision_timestamp: str - - def __post_init__(self) -> None: - """Reject incomplete source metadata before artifact publication.""" - for field, value in ( - ("version", self.version), - ("commit", self.commit), - ("ref", self.ref), - ("revision_timestamp", self.revision_timestamp), - ): - if not value.strip() or value == "unknown": - msg = f"report source {field} is incomplete: {value!r}" - raise ValueError(msg) - - -@dataclass(frozen=True, slots=True) -class ArtifactContext: - """Validated non-tabular metadata for a release-performance dataset.""" - - release: ReleasePair - statistic: Literal["median"] - suite: str - scope: str - source: ReportSource - benchmark_provenance: Mapping[str, object] - - def __post_init__(self) -> None: - """Bind report settings and benchmark provenance to the release pair.""" - if self.statistic != "median": - msg = f"unsupported performance-comparison statistic: {self.statistic!r}" - raise ValueError(msg) - if self.suite not in SUITES: - msg = f"unsupported suite: {self.suite!r}" - raise ValueError(msg) - if self.scope not in SCOPES: - msg = f"unsupported scope: {self.scope!r}" - raise ValueError(msg) - _validate_benchmark_provenance( - self.benchmark_provenance, - context=self, - ) - object.__setattr__(self, "benchmark_provenance", freeze_mapping(self.benchmark_provenance)) - - -@dataclass(frozen=True, slots=True) -class PerformanceBundle: - """A complete validated release-performance dataset and its provenance.""" - - context: ArtifactContext - rows: tuple[PerformanceRow, ...] - - def __post_init__(self) -> None: - """Require non-empty, unique rows bound to one report selection.""" - if not self.rows: - msg = "release-performance dataset must contain at least one row" - raise ValueError(msg) - keys: set[tuple[str, str, str]] = set() - for row in self.rows: - if row.suite not in _selected_suites(self.context.suite): - msg = f"row suite {row.suite!r} is outside selected suite {self.context.suite!r}" - raise ValueError(msg) - if row.scope != self.context.scope: - msg = f"row scope {row.scope!r} does not match report scope {self.context.scope!r}" - raise ValueError(msg) - key = (row.suite, row.scope, row.benchmark_id) - if key in keys: - msg = f"duplicate benchmark key: {key!r}" - raise ValueError(msg) - keys.add(key) - _validate_rational_row_coverage(self.context, self.rows) - - @property - def sorted_rows(self) -> tuple[PerformanceRow, ...]: - """Return rows in deterministic suite/group/benchmark order.""" - return tuple(sorted(self.rows, key=lambda row: (row.suite, row.group, row.benchmark, row.baseline_benchmark))) - - -@dataclass(frozen=True, slots=True) -class ArtifactPaths: - """Adjacent CSV and JSON provenance destinations.""" - - csv: Path - provenance: Path - - def __post_init__(self) -> None: - """Require distinct adjacent artifact files.""" - ensure_distinct_paths({"CSV": self.csv, "provenance": self.provenance}) - if self.csv.resolve(strict=False).parent != self.provenance.resolve(strict=False).parent: - msg = "CSV and provenance sidecar must be adjacent" - raise ValueError(msg) - - -def _selected_suites(suite: str) -> frozenset[str]: - return frozenset({"exact", "vs_linalg"} if suite == "all" else {suite}) - - -def _validate_rational_row_coverage(context: ArtifactContext, rows: tuple[PerformanceRow, ...]) -> None: - """Bind retained rational rows to the explicitly recorded harness capability.""" - validation = context.benchmark_provenance.get("validation") - if not isinstance(validation, Mapping): - msg = "validated benchmark provenance lost its validation object" - raise TypeError(msg) - shared_harness_rational_inputs = validation.get("shared_harness_rational_inputs") - if not isinstance(shared_harness_rational_inputs, bool): - msg = "validated benchmark provenance lost its shared-harness capability" - raise TypeError(msg) - compatibility = resolve_shared_harness_compatibility( - current=context.release.current, - baseline=context.release.baseline, - shared_harness_rational_inputs=shared_harness_rational_inputs, - ) - rational_rows = tuple(row for row in rows if row.group.startswith("rational_input_d")) - expected = compatibility.rational_input_coverage - if expected == "excluded" and rational_rows: - msg = "rational-input rows must be excluded when the shared harness does not emit them" - raise ValueError(msg) - if expected == "current-only" and any(row.coverage_status != "current-only" for row in rational_rows): - msg = "pre-rational baselines require retained rational-input rows to be current-only" - raise ValueError(msg) - if expected == "comparable" and any(row.coverage_status != "comparable" for row in rational_rows): - msg = "rational-capable baselines require retained rational-input rows to be comparable" - raise ValueError(msg) - - -def _paths_alias(first: Path, second: Path) -> bool: - """Return whether two paths resolve to the same filesystem target.""" - if first.exists() and second.exists() and first.samefile(second): - return True - return first.resolve(strict=False) == second.resolve(strict=False) - - -def ensure_distinct_paths(paths: Mapping[str, Path]) -> None: - """Reject lexical, symlink, or existing-file aliases among named paths.""" - items = tuple(paths.items()) - for index, (first_name, first_path) in enumerate(items): - for second_name, second_path in items[index + 1 :]: - if _paths_alias(first_path, second_path): - msg = f"{first_name} and {second_name} must use distinct paths: {first_path}" - raise ValueError(msg) - - -def freeze_json(value: object) -> object: - """Recursively detach and freeze JSON-shaped provenance data.""" - if isinstance(value, Mapping): - return MappingProxyType({str(key): freeze_json(item) for key, item in value.items()}) - if isinstance(value, (list, tuple)): - return tuple(freeze_json(item) for item in value) - return value - - -def freeze_mapping(data: Mapping[str, object]) -> Mapping[str, object]: - """Detach and freeze a JSON-shaped mapping while preserving its mapping type contract.""" - frozen = freeze_json(data) - if not isinstance(frozen, Mapping): - msg = "provenance mapping invariant violated" - raise TypeError(msg) - return cast("Mapping[str, object]", frozen) - - -def _thaw_json(value: object) -> object: - """Convert frozen JSON-shaped data back to serializer-native containers.""" - if isinstance(value, Mapping): - return {str(key): _thaw_json(item) for key, item in value.items()} - if isinstance(value, tuple): - return [_thaw_json(item) for item in value] - return value - - -def _required_provenance_object(data: Mapping[str, object], field: str, *, context: str) -> dict[str, object]: - value = data.get(field) - if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): - msg = f"benchmark provenance requires a {field} object in {context}" - raise TypeError(msg) - return cast("dict[str, object]", value) - - -def _required_provenance_string(data: Mapping[str, object], field: str, *, context: str) -> str: - value = data.get(field) - if not isinstance(value, str) or not value.strip(): - msg = f"benchmark provenance requires a non-empty {context}.{field} string" - raise ValueError(msg) - return value - - -def _required_provenance_sha256(data: Mapping[str, object], field: str, *, context: str) -> str: - value = _required_provenance_string(data, field, context=context) - if len(value) != 64 or any(char not in "0123456789abcdef" for char in value): - msg = f"benchmark provenance {context}.{field} must be a lowercase SHA-256 digest" - raise ValueError(msg) - return value - - -def _required_provenance_bool(data: Mapping[str, object], field: str, *, context: str) -> bool: - value = data.get(field) - if not isinstance(value, bool): - msg = f"benchmark provenance requires a boolean {context}.{field}" - raise TypeError(msg) - return value - - -def _required_provenance_command(data: Mapping[str, object], field: str, *, context: str) -> tuple[str, ...]: - value = data.get(field) - if not isinstance(value, list) or not value or not all(isinstance(part, str) and part for part in value): - msg = f"benchmark provenance requires a non-empty {context}.{field} command" - raise ValueError(msg) - return tuple(cast("list[str]", value)) - - -def _validate_environment_provenance(data: Mapping[str, object], *, context: str) -> None: - for field in ("cpu", "os", "rustc", "commit"): - _required_provenance_string(data, field, context=context) - for field in ("cargo_lock_sha256", "harness_sha256", "source_state_sha256"): - _required_provenance_sha256(data, field, context=context) - if "benchmark_contract_sha256" in data: - _required_provenance_sha256(data, "benchmark_contract_sha256", context=context) - _required_provenance_bool(data, "git_clean", context=context) - gate = _required_provenance_string(data, "correctness_gate", context=context) - if gate != "passed": - msg = f"benchmark provenance {context}.correctness_gate must be 'passed', got {gate!r}" - raise ValueError(msg) - - -def _require_matching_provenance( - first: Mapping[str, object], - second: Mapping[str, object], - field: str, - *, - first_context: str, - second_context: str, -) -> None: - if first.get(field) != second.get(field): - msg = f"benchmark provenance {first_context}.{field} {first.get(field)!r} does not match {second_context}.{field} {second.get(field)!r}" - raise ValueError(msg) - - -def _validate_criterion_provenance(criterion: Mapping[str, object], *, context: ArtifactContext) -> None: - """Validate complete Criterion settings and commands.""" - expected = { - "statistic": context.statistic, - "suite": context.suite, - "scope": context.scope, - "sample": "new", - } - for field, expected_value in expected.items(): - if criterion.get(field) != expected_value: - msg = f"benchmark provenance criterion.{field} {criterion.get(field)!r} does not match report value {expected_value!r}" - raise ValueError(msg) - _required_provenance_string(criterion, "criterion_version", context="criterion") - _required_provenance_command(criterion, "baseline_command", context="criterion") - _required_provenance_command(criterion, "current_command", context="criterion") - - -def _validate_measurement_provenance(measurement: Mapping[str, object], *, mode: object) -> str: - """Validate recorded or explicitly unavailable measurement metadata.""" - measurement_status = _required_provenance_string(measurement, "status", context="measurement") - if measurement_status == "recorded": - if mode != "shared-current-harness": - msg = "recorded benchmark measurement provenance requires shared-current-harness mode" - raise ValueError(msg) - for field in ("cpu", "os", "rustc", "current_commit", "baseline_commit", "baseline_api_compatibility"): - _required_provenance_string(measurement, field, context="measurement") - if cast("str", measurement["cpu"]).casefold() == "unavailable": - msg = "recorded benchmark measurement provenance requires an identified CPU model" - raise ValueError(msg) - for field in ( - "cargo_lock_sha256", - "harness_sha256", - "current_source_state_sha256", - "baseline_source_state_sha256", - ): - _required_provenance_sha256(measurement, field, context="measurement") - if "benchmark_contract_sha256" in measurement: - _required_provenance_sha256(measurement, "benchmark_contract_sha256", context="measurement") - for field in ("current_git_clean", "baseline_git_clean"): - _required_provenance_bool(measurement, field, context="measurement") - elif measurement_status == "unavailable": - _required_provenance_string(measurement, "reason", context="measurement") - else: - msg = f"unsupported benchmark provenance measurement.status: {measurement_status!r}" - raise ValueError(msg) - return measurement_status - - -def _validate_validation_provenance( - validation: Mapping[str, object], - *, - current: str, - baseline: str, -) -> str: - """Validate fixture-gate evidence for both compared revisions.""" - if validation.get("command") != ["just", "test-bench-inputs"]: - msg = "benchmark provenance validation.command must be ['just', 'test-bench-inputs']" - raise ValueError(msg) - for field in ("current_revision", "baseline_revision"): - result = _required_provenance_string(validation, field, context="validation") - if result != "passed": - msg = f"benchmark provenance validation.{field} must be 'passed', got {result!r}" - raise ValueError(msg) - for field in ("current_commit", "baseline_commit"): - _required_provenance_string(validation, field, context="validation") - for field in ("current_source_state_sha256", "baseline_source_state_sha256"): - _required_provenance_sha256(validation, field, context="validation") - for field in ("current_git_clean", "baseline_git_clean"): - _required_provenance_bool(validation, field, context="validation") - compatibility = _required_provenance_string(validation, "baseline_api_compatibility", context="validation") - shared_harness_rational_inputs = validation.get("shared_harness_rational_inputs") - if not isinstance(shared_harness_rational_inputs, bool): - msg = "benchmark provenance validation.shared_harness_rational_inputs must be a boolean" - raise TypeError(msg) - resolved = resolve_shared_harness_compatibility( - current=current, - baseline=baseline, - shared_harness_rational_inputs=shared_harness_rational_inputs, - ) - if compatibility != resolved.baseline_api_compatibility: - msg = ( - "benchmark provenance validation.baseline_api_compatibility must be " - f"{resolved.baseline_api_compatibility!r} for baseline {baseline!r}, got {compatibility!r}" - ) - raise ValueError(msg) - harness = _required_provenance_string(validation, "harness", context="validation") - if harness != "shared-current": - msg = f"benchmark provenance validation.harness must be 'shared-current', got {harness!r}" - raise ValueError(msg) - return compatibility - - -def _validate_current_revision_consistency( - publication: Mapping[str, object], - validation: Mapping[str, object], -) -> None: - """Bind validation of the current revision to publication metadata.""" - for field in ("commit", "git_clean", "source_state_sha256"): - validation_field = f"current_{field}" if field != "commit" else "current_commit" - publication_field = field - if validation.get(validation_field) != publication.get(publication_field): - msg = ( - f"benchmark provenance validation.{validation_field} {validation.get(validation_field)!r} does not match " - f"publication.{publication_field} {publication.get(publication_field)!r}" - ) - raise ValueError(msg) - - -def _validate_recorded_measurement_consistency( - measurement: Mapping[str, object], - publication: Mapping[str, object], - validation: Mapping[str, object], - *, - compatibility: str, -) -> None: - """Bind recorded timing metadata to publication and validation evidence.""" - for field in ("cpu", "os", "rustc", "cargo_lock_sha256", "harness_sha256"): - _require_matching_provenance( - measurement, - publication, - field, - first_context="measurement", - second_context="publication", - ) - if "benchmark_contract_sha256" in measurement or "benchmark_contract_sha256" in publication: - _require_matching_provenance( - measurement, - publication, - "benchmark_contract_sha256", - first_context="measurement", - second_context="publication", - ) - for measurement_field, publication_field in ( - ("current_commit", "commit"), - ("current_git_clean", "git_clean"), - ("current_source_state_sha256", "source_state_sha256"), - ): - if measurement.get(measurement_field) != publication.get(publication_field): - msg = ( - f"benchmark provenance measurement.{measurement_field} {measurement.get(measurement_field)!r} does not match " - f"publication.{publication_field} {publication.get(publication_field)!r}" - ) - raise ValueError(msg) - for field in ("baseline_commit", "baseline_git_clean", "baseline_source_state_sha256"): - _require_matching_provenance( - measurement, - validation, - field, - first_context="measurement", - second_context="validation", - ) - if measurement.get("baseline_api_compatibility") != compatibility: - msg = "benchmark provenance measurement.baseline_api_compatibility does not match validation.baseline_api_compatibility" - raise ValueError(msg) - - -def _validate_benchmark_provenance(data: Mapping[str, object], *, context: ArtifactContext) -> None: - """Reject incomplete or mismatched embedded benchmark provenance.""" - schema = data.get("schema") - if type(schema) is not int or schema != 2: - msg = f"benchmark provenance schema must be 2, got {data.get('schema')!r}" - raise ValueError(msg) - mode = data.get("mode") - if mode not in {"shared-current-harness", "historical-assets"}: - msg = f"unsupported benchmark provenance mode: {mode!r}" - raise ValueError(msg) - if data.get("baseline") != context.release.baseline: - msg = f"benchmark provenance baseline {data.get('baseline')!r} does not match release baseline {context.release.baseline!r}" - raise ValueError(msg) - current = _required_provenance_string(data, "current", context="root") - if current != context.release.current: - msg = f"benchmark provenance current {current!r} does not match release current {context.release.current!r}" - raise ValueError(msg) - - criterion = _required_provenance_object(data, "criterion", context="root") - measurement = _required_provenance_object(data, "measurement", context="root") - publication = _required_provenance_object(data, "publication", context="root") - validation = _required_provenance_object(data, "validation", context="root") - _validate_criterion_provenance(criterion, context=context) - _validate_environment_provenance(publication, context="publication") - source_commit = _required_provenance_string(publication, "commit", context="publication") - if source_commit != "unavailable" and not source_commit.startswith(context.source.commit): - msg = f"benchmark provenance publication.commit {source_commit!r} does not match report source commit {context.source.commit!r}" - raise ValueError(msg) - measurement_status = _validate_measurement_provenance(measurement, mode=mode) - expected_status = "recorded" if mode == "shared-current-harness" else "unavailable" - if measurement_status != expected_status: - msg = f"benchmark provenance mode {mode!r} requires measurement.status {expected_status!r}, got {measurement_status!r}" - raise ValueError(msg) - compatibility = _validate_validation_provenance( - validation, - current=context.release.current, - baseline=context.release.baseline, - ) - _validate_current_revision_consistency(publication, validation) - if measurement_status == "recorded": - _validate_recorded_measurement_consistency( - measurement, - publication, - validation, - compatibility=compatibility, - ) - - -def _timing_fields(prefix: str, estimate: TimingEstimate | None) -> dict[str, str]: - if estimate is None: - return { - f"{prefix}_median_ns": "", - f"{prefix}_ci_lower_ns": "", - f"{prefix}_ci_upper_ns": "", - } - return { - f"{prefix}_median_ns": format(estimate.median_ns, ".17g"), - f"{prefix}_ci_lower_ns": format(estimate.ci_lower_ns, ".17g"), - f"{prefix}_ci_upper_ns": format(estimate.ci_upper_ns, ".17g"), - } - - -def _row_to_csv(row: PerformanceRow) -> dict[str, str]: - values = { - "schema_version": str(SCHEMA_VERSION), - "suite": row.suite, - "scope": row.scope, - "benchmark_id": row.benchmark_id, - "group": row.group, - "benchmark": row.benchmark, - "baseline_benchmark": row.baseline_benchmark, - "coverage_status": row.coverage_status, - "coverage_note": row.coverage_note, - } - values.update(_timing_fields("baseline", row.baseline)) - values.update(_timing_fields("current", row.current)) - values.update(_timing_fields("baseline_nalgebra", row.baseline_nalgebra)) - values.update(_timing_fields("baseline_faer", row.baseline_faer)) - return values - - -def _serialize_csv(bundle: PerformanceBundle) -> bytes: - output = io.StringIO(newline="") - writer = csv.DictWriter(output, fieldnames=CSV_COLUMNS, lineterminator="\n") - writer.writeheader() - for row in bundle.sorted_rows: - writer.writerow(_row_to_csv(row)) - return output.getvalue().encode("utf-8") - - -def _serialize_provenance(bundle: PerformanceBundle, csv_payload: bytes) -> bytes: - context = bundle.context - payload = { - "benchmark_provenance": _thaw_json(context.benchmark_provenance), - "csv": { - "columns": list(CSV_COLUMNS), - "row_count": len(bundle.rows), - "sha256": hashlib.sha256(csv_payload).hexdigest(), - }, - "release": { - "baseline": context.release.baseline, - "current": context.release.current, - }, - "report": { - "scope": context.scope, - "source": { - "commit": context.source.commit, - "ref": context.source.ref, - "revision_timestamp": context.source.revision_timestamp, - "version": context.source.version, - }, - "statistic": context.statistic, - "suite": context.suite, - }, - "schema_version": SCHEMA_VERSION, - } - return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode() - - -def serialize_bundle(bundle: PerformanceBundle) -> tuple[bytes, bytes]: - """Serialize and self-validate one deterministic artifact pair.""" - csv_payload = _serialize_csv(bundle) - provenance_payload = _serialize_provenance(bundle, csv_payload) - parsed = load_bundle_bytes(csv_payload, provenance_payload, source="serialized artifact pair") - if parsed != PerformanceBundle(context=bundle.context, rows=bundle.sorted_rows): - msg = "release-performance artifact round trip changed the validated dataset" - raise ValueError(msg) - return csv_payload, provenance_payload - - -def _parse_required_string(data: Mapping[str, object], field: str, *, source: str) -> str: - value = data.get(field) - if not isinstance(value, str) or not value.strip(): - msg = f"invalid or missing {field} in {source}" - raise ValueError(msg) - return value - - -def _parse_required_object(data: Mapping[str, object], field: str, *, source: str) -> dict[str, object]: - value = data.get(field) - if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): - msg = f"invalid or missing {field} object in {source}" - raise ValueError(msg) - return cast("dict[str, object]", value) - - -def _parse_float(value: str, field: str, *, row_number: int, source: str) -> float: - try: - parsed = float(value) - except ValueError as exc: - msg = f"{field} in CSV row {row_number} of {source} is not numeric: {value!r}" - raise ValueError(msg) from exc - if not math.isfinite(parsed) or parsed <= 0: - msg = f"{field} in CSV row {row_number} of {source} must be finite and positive: {value!r}" - raise ValueError(msg) - return parsed - - -def _parse_timing(row: Mapping[str, str], prefix: str, *, row_number: int, source: str) -> TimingEstimate | None: - field_names = ( - f"{prefix}_median_ns", - f"{prefix}_ci_lower_ns", - f"{prefix}_ci_upper_ns", - ) - values = tuple(row[field] for field in field_names) - if not any(values): - return None - if not all(values): - msg = f"incomplete {prefix} confidence interval in CSV row {row_number} of {source}" - raise ValueError(msg) - return TimingEstimate( - median_ns=_parse_float(values[0], field_names[0], row_number=row_number, source=source), - ci_lower_ns=_parse_float(values[1], field_names[1], row_number=row_number, source=source), - ci_upper_ns=_parse_float(values[2], field_names[2], row_number=row_number, source=source), - ) - - -def _parse_rows(csv_payload: bytes, *, source: str) -> tuple[PerformanceRow, ...]: - try: - text = csv_payload.decode("utf-8") - except UnicodeDecodeError as exc: - msg = f"release-performance CSV is not valid UTF-8 in {source}: {exc}" - raise ValueError(msg) from exc - reader = csv.DictReader(io.StringIO(text, newline="")) - if reader.fieldnames != list(CSV_COLUMNS): - msg = f"unsupported release-performance CSV columns in {source}: {reader.fieldnames!r}" - raise ValueError(msg) - rows: list[PerformanceRow] = [] - for row_number, raw in enumerate(reader, start=2): - if None in raw or any(value is None for value in raw.values()): - msg = f"malformed release-performance CSV row {row_number} in {source}" - raise ValueError(msg) - row = cast("dict[str, str]", raw) - if row["schema_version"] != str(SCHEMA_VERSION): - msg = f"unsupported CSV schema version in row {row_number} of {source}: {row['schema_version']!r}" - raise ValueError(msg) - coverage = row["coverage_status"] - if coverage not in COVERAGE_STATES: - msg = f"unsupported coverage status in row {row_number} of {source}: {coverage!r}" - raise ValueError(msg) - rows.append( - PerformanceRow( - suite=row["suite"], - scope=row["scope"], - benchmark_id=row["benchmark_id"], - group=row["group"], - benchmark=row["benchmark"], - baseline_benchmark=row["baseline_benchmark"], - coverage_status=coverage, - coverage_note=row["coverage_note"], - baseline=_parse_timing(row, "baseline", row_number=row_number, source=source), - current=_parse_timing(row, "current", row_number=row_number, source=source), - baseline_nalgebra=_parse_timing(row, "baseline_nalgebra", row_number=row_number, source=source), - baseline_faer=_parse_timing(row, "baseline_faer", row_number=row_number, source=source), - ) - ) - return tuple(rows) - - -def _parse_provenance(payload: bytes, *, source: str) -> tuple[ArtifactContext, str, int, tuple[str, ...]]: - try: - raw = json.loads(payload) - except (UnicodeDecodeError, json.JSONDecodeError) as exc: - msg = f"malformed release-performance provenance JSON in {source}: {exc}" - raise ValueError(msg) from exc - if not isinstance(raw, dict): - msg = f"release-performance provenance must be a JSON object in {source}" - raise TypeError(msg) - data = cast("dict[str, object]", raw) - schema_version = data.get("schema_version") - if type(schema_version) is not int or schema_version != SCHEMA_VERSION: - msg = f"unsupported release-performance provenance schema in {source}: {data.get('schema_version')!r}" - raise ValueError(msg) - - release_data = _parse_required_object(data, "release", source=source) - report_data = _parse_required_object(data, "report", source=source) - source_data = _parse_required_object(report_data, "source", source=source) - csv_data = _parse_required_object(data, "csv", source=source) - benchmark_provenance = _parse_required_object(data, "benchmark_provenance", source=source) - - release = ReleasePair( - current=_parse_required_string(release_data, "current", source=source), - baseline=_parse_required_string(release_data, "baseline", source=source), - ) - benchmark_provenance.setdefault("current", release.current) - validation = benchmark_provenance.get("validation") - if isinstance(validation, dict): - validation.setdefault("shared_harness_rational_inputs", False) - statistic = _parse_required_string(report_data, "statistic", source=source) - if statistic != "median": - msg = f"unsupported report statistic in {source}: {statistic!r}" - raise ValueError(msg) - context = ArtifactContext( - release=release, - statistic="median", - suite=_parse_required_string(report_data, "suite", source=source), - scope=_parse_required_string(report_data, "scope", source=source), - source=ReportSource( - version=_parse_required_string(source_data, "version", source=source), - commit=_parse_required_string(source_data, "commit", source=source), - ref=_parse_required_string(source_data, "ref", source=source), - revision_timestamp=_parse_required_string(source_data, "revision_timestamp", source=source), - ), - benchmark_provenance=benchmark_provenance, - ) - expected_current = f"v{context.source.version.removeprefix('v')}" - if context.release.current != expected_current: - msg = f"release current {context.release.current!r} does not match report source version {context.source.version!r} in {source}" - raise ValueError(msg) - - digest = _parse_required_string(csv_data, "sha256", source=source) - if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest): - msg = f"invalid csv.sha256 in {source}" - raise ValueError(msg) - row_count = csv_data.get("row_count") - if isinstance(row_count, bool) or not isinstance(row_count, int) or row_count <= 0: - msg = f"invalid csv.row_count in {source}: {row_count!r}" - raise ValueError(msg) - columns = csv_data.get("columns") - if columns != list(CSV_COLUMNS): - msg = f"provenance CSV columns do not match schema {SCHEMA_VERSION} in {source}" - raise ValueError(msg) - return context, digest, row_count, tuple(cast("list[str]", columns)) - - -def load_bundle_bytes(csv_payload: bytes, provenance_payload: bytes, *, source: str) -> PerformanceBundle: - """Parse and validate an artifact pair before it reaches report rendering.""" - context, expected_digest, expected_count, _columns = _parse_provenance(provenance_payload, source=source) - try: - verify_sha256(csv_payload, expected_digest) - except ValueError as exc: - raise ValueError(f"CSV digest mismatch in {source}: {exc}") from exc - rows = _parse_rows(csv_payload, source=source) - if len(rows) != expected_count: - msg = f"CSV row count mismatch in {source}: expected {expected_count}, got {len(rows)}" - raise ValueError(msg) - return PerformanceBundle(context=context, rows=rows) - - -def load_bundle(paths: ArtifactPaths) -> PerformanceBundle: - """Load an adjacent CSV/provenance pair and fail closed on partial publication.""" - missing = [str(path) for path in (paths.csv, paths.provenance) if not path.is_file()] - if missing: - msg = f"release-performance artifact pair is incomplete; missing: {', '.join(missing)}" - raise FileNotFoundError(msg) - return load_bundle_bytes( - paths.csv.read_bytes(), - paths.provenance.read_bytes(), - source=f"{paths.csv} and {paths.provenance}", - ) - - -def bundle_outputs(paths: ArtifactPaths, bundle: PerformanceBundle) -> dict[Path, bytes]: - """Validate serialized legacy evidence before composing its publication.""" - ensure_distinct_paths({"artifact CSV": paths.csv, "artifact provenance": paths.provenance}) - csv_payload, provenance_payload = serialize_bundle(bundle) - return {paths.csv: csv_payload, paths.provenance: provenance_payload} - - -def write_bundle(paths: ArtifactPaths, bundle: PerformanceBundle) -> None: - """Publish the complete validated artifact pair through the shared transaction.""" - replace_many(bundle_outputs(paths, bundle)) diff --git a/scripts/performance_runs.py b/scripts/performance_runs.py index f95f53a..c32e063 100644 --- a/scripts/performance_runs.py +++ b/scripts/performance_runs.py @@ -2,30 +2,181 @@ import json import os +import re import tomllib -from dataclasses import replace +from dataclasses import dataclass, replace from pathlib import Path -from typing import TYPE_CHECKING +from types import MappingProxyType +from typing import TYPE_CHECKING, cast from research_repo_tools.common_measurement import CommonHarnessPlan, MeasurementPhase from research_repo_tools.complete_runs import CompletePolicy, CompleteRun, RunSeries, validate_run_evidence +from research_repo_tools.criterion_inventory import DiscoveryCommand, DiscoveryPlan, discover_inventory, load_discovery from research_repo_tools.evidence import Evidence, fingerprint_files, load_evidence from research_repo_tools.measurement import MeasurementConfig, load_measurement -from research_repo_tools.process import run_command +from research_repo_tools.release_discovery import normalize_tag from research_repo_tools.release_pairs import ReleasePair from research_repo_tools.run_reports import load_latest_run -import bench_compare as report -from benchmark_contract import find_project_root -from performance_artifacts import NO_API_COMPATIBILITY, resolve_shared_harness_compatibility -from release_baseline import parse_benchmark_list - if TYPE_CHECKING: - from research_repo_tools.criterion import Estimate + from collections.abc import Mapping REPORT_CONFIG = "tooling/performance-report.toml" MEASUREMENT_CONFIG = "tooling/performance.toml" -UNAVAILABLE_PREFIXES = ("rational_input_", "canonical_conversion_", "det4_diagnostic_") +POLICY_CONFIG = "tooling/performance-policy.toml" +NO_API_COMPATIBILITY = "none" +type ReleaseVersion = tuple[int, int, int] + + +@dataclass(frozen=True) +class PlotMetric: + """One scientific operation and its current/baseline peer identifiers.""" + + title: str + benchmarks: tuple[str, str, str] + + +@dataclass(frozen=True) +class ScientificPolicy: + """Parsed, immutable consumer declarations used by every performance path.""" + + dimensions: tuple[int, ...] + minimum_release: ReleaseVersion + modern_api_release: ReleaseVersion + legacy_api_cfg: str + library_benchmarks: tuple[str, ...] + unavailable_prefixes: tuple[str, ...] + unavailable_benchmarks: tuple[str, ...] + unavailable_benchmark_prefixes: tuple[str, ...] + exact: Mapping[str, tuple[str, ...]] + additional: Mapping[str, tuple[str, ...]] + peers: Mapping[str, tuple[str, str]] + metrics: Mapping[str, PlotMetric] + + +def _names(value: object, field: str, *, length: int | None = None) -> tuple[str, ...]: + """Reject malformed or ambiguous Criterion names before constructing rows.""" + if not isinstance(value, list) or any(not isinstance(item, str) or re.fullmatch(r"[A-Za-z0-9_]+", item) is None for item in value): + raise ValueError(f"{field} requires a list of Criterion names") + names = cast("tuple[str, ...]", tuple(value)) + if len(set(names)) != len(names) or (length is not None and len(names) != length): + raise ValueError(f"{field} requires distinct names" + (f" with exactly {length} entries" if length is not None else "")) + return names + + +def _table(value: object, field: str) -> dict[str, object]: + """Admit only named catalog tables, leaving their values to owning parsers.""" + if not isinstance(value, dict) or any(not isinstance(key, str) or re.fullmatch(r"[A-Za-z0-9_]+", key) is None for key in value): + raise ValueError(f"{field} requires a named catalog table") + return cast("dict[str, object]", value) + + +def _dimensions(value: object) -> tuple[int, ...]: + """Keep dimensions positive, unique and in the declared plotting order.""" + if not isinstance(value, list) or not value or any(type(item) is not int or item <= 0 for item in value): + msg = "dimensions requires positive integers" + raise ValueError(msg) + dimensions = cast("tuple[int, ...]", tuple(value)) + if tuple(sorted(set(dimensions))) != dimensions: + msg = "dimensions requires increasing, distinct integers" + raise ValueError(msg) + return dimensions + + +def _metrics(value: object, peers: Mapping[str, tuple[str, str]]) -> Mapping[str, PlotMetric]: + """Bind each plotted operation to its declared pair of baseline peers.""" + metrics = {} + for name, record in _table(value, "metrics").items(): + fields = _table(record, f"metrics.{name}") + title = fields.get("title") + if not isinstance(title, str) or not title.strip(): + raise ValueError(f"metrics.{name}.title requires nonempty text") + benchmarks = cast("tuple[str, str, str]", _names(fields.get("benchmarks"), f"metrics.{name}.benchmarks", length=3)) + if peers.get(benchmarks[0]) != benchmarks[1:]: + raise ValueError(f"metrics.{name}.benchmarks differs from the declared peer pair") + metrics[name] = PlotMetric(title, benchmarks) + if not metrics: + msg = "metrics requires at least one operation" + raise ValueError(msg) + return MappingProxyType(metrics) + + +def _release_threshold(value: object, field: str) -> ReleaseVersion: + """Parse one canonical, nonnegative release threshold from TOML.""" + if not isinstance(value, list) or len(value) != 3 or any(type(item) is not int or item < 0 for item in value): + raise ValueError(f"{field} requires three nonnegative integers") + return cast("ReleaseVersion", tuple(value)) + + +def find_project_root(start: Path | None = None) -> Path: + """Resolve consumer commands from their Cargo checkout.""" + directory = (start or Path.cwd()).resolve() + for candidate in (directory, *directory.parents): + if (candidate / "Cargo.toml").is_file(): + return candidate + msg = "Could not locate Cargo.toml to determine benchmark root" + raise FileNotFoundError(msg) + + +def scientific_policy(root: Path | None = None) -> ScientificPolicy: + """Parse the consumer-owned scientific catalog before discovery or measurement.""" + policy = tomllib.loads(((root or find_project_root()) / POLICY_CONFIG).read_text(encoding="utf-8")) + if type(policy.get("schema")) is not int or policy["schema"] != 1: + msg = "scientific policy requires schema 1" + raise ValueError(msg) + minimum = _release_threshold(policy.get("minimum-release"), "minimum-release") + modern = _release_threshold(policy.get("modern-api-release"), "modern-api-release") + if modern < minimum: + msg = "modern API release cannot precede the minimum release" + raise ValueError(msg) + cfg = policy.get("legacy-api-cfg") + if not isinstance(cfg, str) or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", cfg) is None: + msg = "legacy API cfg requires a Rust identifier" + raise ValueError(msg) + dimensions = _dimensions(policy.get("dimensions")) + library = _names(policy.get("library-benchmarks"), "library-benchmarks") + exact = {name: _names(names, f"exact.{name}") for name, names in _table(policy.get("exact"), "exact").items()} + if not library or not exact or any(not names for names in exact.values()): + msg = "scientific policy requires library and exact benchmark selections" + raise ValueError(msg) + additional = {dimension: _names(names, f"additional.{dimension}") for dimension, names in _table(policy.get("additional"), "additional").items()} + if not set(additional) <= {str(value) for value in dimensions}: + msg = "additional benchmark dimensions must occur in dimensions" + raise ValueError(msg) + peers = {name: cast("tuple[str, str]", _names(names, f"peers.{name}", length=2)) for name, names in _table(policy.get("peers"), "peers").items()} + if not set(peers) <= {*library, *(name for names in additional.values() for name in names)}: + msg = "peers must refer to declared library benchmarks" + raise ValueError(msg) + return ScientificPolicy( + dimensions=dimensions, + minimum_release=minimum, + modern_api_release=modern, + legacy_api_cfg=cfg, + library_benchmarks=library, + unavailable_prefixes=_names(policy.get("unavailable-prefixes"), "unavailable-prefixes"), + unavailable_benchmarks=_names(policy.get("unavailable-benchmarks"), "unavailable-benchmarks"), + unavailable_benchmark_prefixes=_names(policy.get("unavailable-benchmark-prefixes"), "unavailable-benchmark-prefixes"), + exact=MappingProxyType(exact), + additional=MappingProxyType(additional), + peers=MappingProxyType(peers), + metrics=_metrics(policy.get("metrics"), peers), + ) + + +def compatibility_adapter(pair: ReleasePair, root: Path | None = None) -> str: + """Reject excluded releases and select the historical API cfg.""" + policy = scientific_policy(root) + versions = {phase: tuple(map(int, normalize_tag(tag)[1:].split("."))) for phase, tag in (("baseline", pair.baseline), ("current", pair.current))} + if any(version < policy.minimum_release for version in versions.values()): + minimum_release = ".".join(map(str, policy.minimum_release)) + msg = f"unsupported comparison release; requires v{minimum_release} or newer" + raise ValueError(msg) + return policy.legacy_api_cfg if versions["baseline"] < policy.modern_api_release else NO_API_COMPATIBILITY + + +def required_report_ids(root: Path | None = None) -> set[str]: + """Keep release discovery accountable to every report and plot selection.""" + return set(load_discovery(root or find_project_root(), "tooling/criterion-inventory.toml").required) def configuration(root: Path | None = None) -> MeasurementConfig: @@ -46,19 +197,20 @@ def archive_directory(root: Path) -> str: return archive -def selected_ids(suite: str, scope: str, expected: tuple[str, ...]) -> tuple[str, ...]: +def selected_ids(suite: str, scope: str, expected: tuple[str, ...], root: Path | None = None) -> tuple[str, ...]: """Require the repository report registry, keeping diagnostic cases in the run.""" if suite not in {"all", "exact", "vs_linalg"} or scope not in {"all-benches", "release-signal"}: msg = "unknown la-stack suite or report scope" raise ValueError(msg) if scope == "all-benches": return tuple(name for name in expected if not name.split("/")[0].removeprefix("d").isdigit() or "/la_stack_" in name) + policy = scientific_policy(root) required = { - *(f"{group}/{bench}" for group, benches in report.EXACT_GROUPS.items() for bench in benches if suite != "vs_linalg"), + *(f"{group}/{bench}" for group, benches in policy.exact.items() for bench in benches if suite != "vs_linalg"), *( f"d{dimension}/{bench}" - for dimension in report.VS_LINALG_CANONICAL_DIMS - for bench in (*report.VS_LINALG_LA_STACK_BENCHES, *report.VS_LINALG_RELEASE_SIGNAL_BENCHES_BY_DIM.get(dimension, [])) + for dimension in policy.dimensions + for bench in (*policy.library_benchmarks, *policy.additional.get(str(dimension), ())) if suite != "exact" ), } @@ -81,20 +233,21 @@ def phase_environment(root: Path, adapter: str = NO_API_COMPATIBILITY) -> tuple[ return tuple(sorted(env.items())) -def _historical_case_available(full_id: str) -> bool: +def _historical_case_available(full_id: str, policy: ScientificPolicy) -> bool: """Mirror the groups and norm cases disabled by the historical harness cfg.""" bench = full_id.partition("/")[2] return ( - not full_id.startswith(UNAVAILABLE_PREFIXES) - and bench != "la_stack_norm2" - and not bench.startswith(("la_stack_norm2_scenario_", "iterative_f64_hypot_norm2_scenario_", "delaunay_scaled_norm_norm2_scenario_")) + not full_id.startswith(policy.unavailable_prefixes) + and bench not in policy.unavailable_benchmarks + and not bench.startswith(policy.unavailable_benchmark_prefixes) ) -def measurement_plan(root: Path, pair: ReleasePair, suite: str, scope: str, listing: Path) -> CommonHarnessPlan: +def measurement_plan(root: Path, pair: ReleasePair, suite: str, scope: str) -> CommonHarnessPlan: """Build the shared plan from native inventories and consumer API policy.""" - compatibility = resolve_shared_harness_compatibility(current=pair.current, baseline=pair.baseline, shared_harness_rational_inputs=True) - baseline_env = phase_environment(root, compatibility.baseline_api_compatibility) + adapter = compatibility_adapter(pair, root) + policy = scientific_policy(root) + baseline_env = phase_environment(root, adapter) current_env = phase_environment(root) config = configuration(root) inventories = {} @@ -102,18 +255,18 @@ def measurement_plan(root: Path, pair: ReleasePair, suite: str, scope: str, list # Discover against the current API, then apply the exact groups excluded # by the historical cfg. Old method names cannot compile on current code. # The shared collector later checks this entire inventory on each source. - env = os.environ | dict(current_env) | {"CRITERION_HOME": str(listing / phase)} - ids = [] - for selected in ("vs_linalg", "exact") if suite == "all" else (suite,): - command = (*config.command[:-1], f"{phase}-{selected}", "list") - result = run_command(command[0], command[1:], cwd=root, env=env, timeout=config.timeout) - ids.extend(parse_benchmark_list(result.stdout)) - if len(ids) != len(set(ids)): - msg = "duplicate semantic benchmark IDs" - raise ValueError(msg) - old_api = phase == "baseline" and compatibility.baseline_api_compatibility != NO_API_COMPATIBILITY - inventories[phase] = tuple(sorted(name for name in ids if not old_api or _historical_case_available(name))) - return declared_plan(pair, suite, scope, inventories, baseline_env=baseline_env, current_env=current_env, config=config) + declarations = load_discovery(root, "tooling/criterion-inventory.toml") + discovery = DiscoveryPlan( + tuple( + DiscoveryCommand(item.name, (*config.command[:-1], f"{phase}-{item.name}", "list"), timeout=config.timeout) + for item in declarations.suites + if suite in {"all", item.name} + ) + ) + ids = discover_inventory(root, discovery, env=os.environ | dict(current_env)).expected + old_api = phase == "baseline" and adapter != NO_API_COMPATIBILITY + inventories[phase] = tuple(sorted(name for name in ids if not old_api or _historical_case_available(name, policy))) + return declared_plan(pair, suite, scope, inventories, baseline_env=baseline_env, current_env=current_env, config=config, root=root) def declared_plan( # noqa: PLR0913 @@ -125,13 +278,15 @@ def declared_plan( # noqa: PLR0913 baseline_env: tuple[tuple[str, str], ...], current_env: tuple[tuple[str, str], ...], config: MeasurementConfig | None = None, + root: Path | None = None, ) -> CommonHarnessPlan: """Bind exact case inventories, reference phases, commands and completeness.""" - config = config or configuration() - compatibility = resolve_shared_harness_compatibility(current=pair.current, baseline=pair.baseline, shared_harness_rational_inputs=True) - selected = selected_ids(suite, scope, inventories["current"]) - unavailable = compatibility.baseline_api_compatibility != NO_API_COMPATIBILITY - baseline_rows = tuple(name for name in selected if not unavailable or _historical_case_available(name)) + config = config or configuration(root) + policy = scientific_policy(root) + adapter = compatibility_adapter(pair, root) + selected = selected_ids(suite, scope, inventories["current"], root) + unavailable = adapter != NO_API_COMPATIBILITY + baseline_rows = tuple(name for name in selected if not unavailable or _historical_case_available(name, policy)) missing = set(baseline_rows) - set(inventories["baseline"]) if missing: raise ValueError(f"baseline inventory omits selected rows: {sorted(missing)}") @@ -139,11 +294,10 @@ def declared_plan( # noqa: PLR0913 RunSeries("la-stack baseline", "baseline", tuple((name, name) for name in baseline_rows)), RunSeries("la-stack current", "current", tuple((name, name) for name in selected)), ] + peers = policy.peers for library, peer_index in (("nalgebra", 0), ("faer", 1)): rows = tuple( - (name, f"{name.split('/')[0]}/{report.VS_LINALG_BASELINE_PEERS[name.split('/')[1]][peer_index]}") - for name in selected - if name.startswith("d") and name.split("/")[1] in report.VS_LINALG_BASELINE_PEERS + (name, f"{name.split('/')[0]}/{peers[name.split('/')[1]][peer_index]}") for name in selected if name.startswith("d") and name.split("/")[1] in peers ) if rows: series.append(RunSeries(library, "baseline", rows)) @@ -163,7 +317,7 @@ def declared_plan( # noqa: PLR0913 *config.context, ("suite", suite), ("scope", scope), - ("baseline-api-compatibility", compatibility.baseline_api_compatibility), + ("baseline-api-compatibility", adapter), ), ) return CommonHarnessPlan(config, phases["baseline"], phases["current"], tuple(series)) @@ -181,13 +335,13 @@ def _legacy_cargo_commands(suite: str, phase: str) -> str: ) -def validate_scientific_run(evidence: Evidence, *, release: bool = False) -> CompleteRun: # noqa: C901 +def validate_scientific_run(evidence: Evidence, *, release: bool = False, root: Path | None = None) -> CompleteRun: # noqa: C901 """Require la-stack's sampling, selected coverage and original reference phases.""" run = validate_run_evidence(evidence) sources = dict(evidence.sources) context = dict(sources["current"].context) pair = ReleasePair(context["release"], dict(sources["baseline"].context)["release"]) - compatibility = resolve_shared_harness_compatibility(current=pair.current, baseline=pair.baseline, shared_harness_rational_inputs=True) + adapter = compatibility_adapter(pair, root) if release and pair.current == pair.baseline: msg = "cannot promote a same-version local comparison as release documentation" raise ValueError(msg) @@ -200,9 +354,15 @@ def validate_scientific_run(evidence: Evidence, *, release: bool = False) -> Com msg = "la-stack requires 100 samples, mean and median, and 95% intervals in ns" raise ValueError(msg) expected = declared_plan( - pair, context["suite"], context["scope"], {phase: sample.policy.expected for phase, sample in samples.items()}, baseline_env=(), current_env=() + pair, + context["suite"], + context["scope"], + {phase: sample.policy.expected for phase, sample in samples.items()}, + baseline_env=(), + current_env=(), + root=root, ) - if run.series != expected.series or context["baseline-api-compatibility"] != compatibility.baseline_api_compatibility: + if run.series != expected.series or context["baseline-api-compatibility"] != adapter: msg = "retained series or compatibility policy differs from la-stack declarations" raise ValueError(msg) legacy = "phase-driver" not in context @@ -219,7 +379,8 @@ def validate_scientific_run(evidence: Evidence, *, release: bool = False) -> Com if actual.get("phase-driver") != context.get("phase-driver") or any(actual.get(key) != value for key, value in required.items()): msg = "retained phase differs from the declared consumer policy" raise ValueError(msg) - for key, command in (("command", getattr(expected, phase).command), ("gate-command", getattr(expected, phase).gate)): + for key, native_command in (("command", getattr(expected, phase).command), ("gate-command", getattr(expected, phase).gate)): + command = native_command recorded = tuple(json.loads(actual[key])) if legacy: # Interpreter paths varied across hosts. Old evidence remains @@ -243,32 +404,30 @@ def retained_run(root: Path, stem: Path) -> Evidence: # Partial new or legacy scratch must never silently select older history. present = any(path.exists() or path.is_symlink() for path in (*paths, *legacy)) evidence = load_evidence(*paths) if present else load_latest_run(root, archive_directory(root)) - validate_scientific_run(evidence) + validate_scientific_run(evidence, root=root) return evidence -def verify_current_source(root: Path, evidence: Evidence) -> None: +def verify_current_source(root: Path, evidence: Evidence) -> dict[str, bytes]: """Bind README publication to the measured current library and captured harness.""" source = dict(evidence.sources)["current"] context = dict(source.context) config = configuration(root) + inputs: dict[str, bytes] = {} for key, expected in (("source-inventory", source.source_sha256), ("harness-inventory", source.harness_sha256)): - names = json.loads(context[key]) + names = cast("list[str]", json.loads(context[key])) patterns = config.sources if key == "source-inventory" else config.harness actual = {path.relative_to(root).as_posix() for pattern in patterns for path in root.glob(pattern)} if actual != set(names): raise ValueError(f"current {key} has stale file membership") - if fingerprint_files(root, tuple(Path(name) for name in names)) != expected: + observed = {name: (root / name).read_bytes() for name in names} + if fingerprint_files(root, tuple(Path(name) for name in names)) != expected or any( + (root / name).read_bytes() != data for name, data in observed.items() + ): raise ValueError(f"current {key} is stale relative to retained measurements") - version = tomllib.loads((root / "Cargo.toml").read_text(encoding="utf-8"))["package"]["version"] + inputs.update(observed) + version = tomllib.loads(inputs["Cargo.toml"].decode("utf-8"))["package"]["version"] if f"v{version}" != context["release"]: msg = "retained release does not match the current package" raise ValueError(msg) - - -def estimates_by_series(run: CompleteRun, statistic: str = "median") -> dict[str, dict[str, Estimate]]: - """Select each named view from its actual measurement phase.""" - if statistic not in {"mean", "median"}: - raise ValueError(f"unsupported statistic: {statistic}") - phases = {phase: {case.full_id: case for case in sample.cases} for phase, sample in run.phases} - return {series.name: {label: phases[series.phase][full_id].estimate(statistic) for label, full_id in series.rows} for series in run.series} + return inputs diff --git a/scripts/release_baseline.py b/scripts/release_baseline.py deleted file mode 100644 index f3227fc..0000000 --- a/scripts/release_baseline.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Inventory and validate complete release Criterion datasets before packaging.""" - -import argparse -import json -import os -import re -import subprocess -import sys -from pathlib import Path - -from research_repo_tools.complete_runs import CompletePolicy, collect_complete_sample -from research_repo_tools.process import ExecutableNotFoundError, format_exception_diagnostics, run_command - -from bench_compare import ( - EXACT_GROUPS, - VS_LINALG_CANONICAL_DIMS, - VS_LINALG_RELEASE_SIGNAL_BENCHES_BY_DIM, - VS_LINALG_STANDARD_BENCH_ORDER, -) - -SUITES = ("vs_linalg", "exact") -RAW_FILES = ("benchmark.json", "estimates.json", "sample.json", "tukey.json") - - -def parse_benchmark_list(output: str) -> list[str]: - """Read Criterion's full IDs, rejecting empty or duplicate inventories.""" - ids = [line.removesuffix(": benchmark") for line in output.splitlines() if line.endswith(": benchmark")] - if not ids or len(ids) != len(set(ids)) or any("/" not in name for name in ids): - msg = "Criterion inventory must contain unique, nonempty benchmark IDs" - raise ValueError(msg) - return sorted(ids) - - -def required_report_ids() -> set[str]: - """Include every release-report row and all canonical README peer rows.""" - return { - *(f"{group}/{bench}" for group, benches in EXACT_GROUPS.items() for bench in benches), - *( - f"d{dimension}/{bench}" - for dimension in VS_LINALG_CANONICAL_DIMS - for bench in (*VS_LINALG_STANDARD_BENCH_ORDER, *VS_LINALG_RELEASE_SIGNAL_BENCHES_BY_DIM.get(dimension, [])) - ), - } - - -def inventory_ids(data: object) -> set[str]: - """Validate the manifest and its coverage of the report consumers.""" - if not isinstance(data, dict) or set(data) != set(SUITES): - msg = "inventory must contain exactly vs_linalg and exact suites" - raise ValueError(msg) - expected: set[str] = set() - for suite, ids in data.items(): - if not isinstance(ids, list) or not ids or any(not isinstance(name, str) or not name for name in ids): - raise ValueError(f"invalid benchmark inventory for {suite}") - if len(ids) != len(set(ids)) or expected.intersection(ids): - raise ValueError(f"duplicate benchmark IDs in {suite}") - expected.update(ids) - missing = required_report_ids() - expected - if missing: - raise ValueError(f"inventory omits report consumers: {', '.join(sorted(missing))}") - return expected - - -def discover(root: Path, manifest: Path, criterion: Path) -> None: - """Compile and list both unfiltered suites without collecting measurements.""" - if criterion.exists() and any(criterion.iterdir()): - raise ValueError(f"release inventory requires a fresh Criterion directory: {criterion}") - inventory: dict[str, list[str]] = {} - for suite in SUITES: - print(f"[release-baseline] Discovering {suite}", flush=True) - result = run_command( - "just", - ["--justfile", "tooling/performance.just", "--working-directory", ".", f"baseline-{suite}", "list"], - cwd=root, - timeout=None, # The workflow bounds compilation and discovery together. - ) - inventory[suite] = parse_benchmark_list(result.stdout) - count = len(inventory[suite]) - print( - f"[release-baseline] {suite}: {count} benchmarks; " - f"nominal warmup + measurement {count * 8 / 60:.1f} min; " - f"planning estimate at 12 s/benchmark {count * 12 / 60:.1f} min (not an upper bound)", - flush=True, - ) - inventory_ids(inventory) - manifest.parent.mkdir(parents=True, exist_ok=True) - manifest.write_text(json.dumps(inventory, indent=2) + "\n", encoding="utf-8", newline="\n") - - -def validate(criterion: Path, manifest: Path, baseline: str) -> int: - """Fail closed on missing, corrupt, stale, or unexpected benchmark output.""" - if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", baseline) or baseline in {"new", "base", "change", "report"}: - msg = "baseline must be a safe, nonreserved directory name" - raise ValueError(msg) - expected = inventory_ids(json.loads(manifest.read_text(encoding="utf-8"))) - report_ids = required_report_ids() - complete = collect_complete_sample(criterion, "new", CompletePolicy(tuple(sorted(expected)))) - for case in complete.cases: - benchmark = case.full_id - directory = criterion / case.storage_path / "new" - if benchmark in report_ids and directory.parent != criterion / benchmark: - raise ValueError(f"report benchmark is outside its consumer path: {directory}") - saved = directory.parent / baseline - for filename in RAW_FILES: - # Criterion can log a write/copy error and still exit successfully. - # Require every saved raw file to match the freshly measured sample. - source = directory / filename - destination = saved / filename - if source.read_bytes() != destination.read_bytes(): - raise ValueError(f"saved baseline differs from new measurement: {destination}") - json.loads(source.read_text(encoding="utf-8")) - return len(complete.cases) - - -def main(argv: list[str] | None = None) -> int: - """Provide the release workflow's inventory and publication gates.""" - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("command", choices=("inventory", "validate")) - parser.add_argument("--baseline") - parser.add_argument("--root", type=Path, default=Path.cwd()) - parser.add_argument("--manifest", type=Path, default=Path("target/release-benchmark-inventory.json")) - parser.add_argument("--criterion-dir", type=Path, default=Path(os.environ.get("CRITERION_HOME", "target/criterion"))) - args = parser.parse_args(argv) - try: - if args.command == "inventory": - discover(args.root, args.manifest, args.criterion_dir) - else: - if not args.baseline: - parser.error("validate requires --baseline") - count = validate(args.criterion_dir, args.manifest, args.baseline) - print(f"[release-baseline] Validated {count} complete benchmarks in new and {args.baseline}") - return 0 - except (OSError, ValueError, TypeError, ExecutableNotFoundError, subprocess.SubprocessError) as error: - print(f"[release-baseline] {format_exception_diagnostics(error)}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/tests/test_bench_compare.py b/scripts/tests/test_bench_compare.py deleted file mode 100644 index dee1e79..0000000 --- a/scripts/tests/test_bench_compare.py +++ /dev/null @@ -1,1363 +0,0 @@ -"""Tests for exact-arithmetic benchmark comparison reports.""" - -import json -import subprocess -from pathlib import Path -from types import SimpleNamespace -from typing import cast - -import pytest - -import bench_compare - -_OVERFLOWING_TIMING = 10**400 - - -def _write_estimates( - path: Path, - stat: str, - median: float, - *, - lower: float | None = None, - upper: float | None = None, -) -> None: - """Write a minimal Criterion estimates.json.""" - lower = median * 0.9 if lower is None else lower - upper = median * 1.1 if upper is None else upper - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - json.dumps( - { - stat: { - "point_estimate": median, - "confidence_interval": {"lower_bound": lower, "upper_bound": upper}, - } - } - ), - encoding="utf-8", - ) - - -def _write_harness_provenance( - criterion_dir: Path, - *, - sha256: str = "a" * 64, - baseline: str = "v0.4.5", -) -> None: - """Write valid shared-harness provenance metadata.""" - criterion_dir.mkdir(parents=True, exist_ok=True) - (criterion_dir / ".la-stack-benchmark-harness.json").write_text( - json.dumps( - { - "schema": 1, - "mode": "shared-current-harness", - "sha256": sha256, - "baseline": baseline, - } - ), - encoding="utf-8", - ) - - -def _schema2_provenance_data() -> dict[str, object]: - environment: dict[str, object] = { - "cargo_lock_sha256": "b" * 64, - "commit": "current-commit", - "correctness_gate": "passed", - "cpu": "test-cpu", - "git_clean": False, - "harness_sha256": "a" * 64, - "os": "TestOS 1 x86_64", - "rustc": "rustc 1.88.0", - "source_state_sha256": "c" * 64, - } - return { - "baseline": "v0.4.5", - "current": "v0.4.6", - "criterion": { - "baseline_command": ["just", "bench-save-baseline", "v0.4.5"], - "criterion_version": "0.7.0", - "current_command": ["just", "bench-latest"], - "sample": "new", - "scope": "release-signal", - "statistic": "median", - "suite": "all", - }, - "measurement": { - "baseline_api_compatibility": "la_stack_pre_rational_input_api", - "baseline_commit": "baseline-commit", - "baseline_git_clean": False, - "baseline_source_state_sha256": "d" * 64, - "cargo_lock_sha256": "b" * 64, - "cpu": "test-cpu", - "current_commit": "current-commit", - "current_git_clean": False, - "current_source_state_sha256": "c" * 64, - "harness_sha256": "a" * 64, - "os": "TestOS 1 x86_64", - "rustc": "rustc 1.88.0", - "status": "recorded", - }, - "mode": "shared-current-harness", - "publication": environment, - "schema": 2, - "validation": { - "baseline_api_compatibility": "la_stack_pre_rational_input_api", - "baseline_commit": "baseline-commit", - "baseline_git_clean": False, - "baseline_revision": "passed", - "baseline_source_state_sha256": "d" * 64, - "command": ["just", "test-bench-inputs"], - "current_commit": "current-commit", - "current_git_clean": False, - "current_revision": "passed", - "current_source_state_sha256": "c" * 64, - "harness": "shared-current", - "shared_harness_rational_inputs": True, - }, - } - - -def _read_harness_provenance( # noqa: PLR0913 - criterion_dir: Path, - *, - baseline: str = "v0.4.5", - suite: bench_compare.BenchmarkSuite = "all", - scope: bench_compare.ComparisonScope = "release-signal", - stat: bench_compare.Statistic = "median", - sample: str = "new", -) -> bench_compare.HarnessProvenance | None: - return bench_compare._read_harness_provenance( - criterion_dir, - expected_baseline=baseline, - expected=bench_compare.CriterionSelection( - suite=suite, - scope=scope, - statistic=stat, - sample=sample, - ), - ) - - -def _build_criterion_tree(criterion_dir: Path, stat: str = "median") -> None: - """Create a fake Criterion directory with exact benchmark results.""" - for d, det, det_exact in [(2, 1.0, 4000.0), (3, 5.0, 21000.0)]: - group = criterion_dir / f"exact_d{d}" - _write_estimates(group / "det" / "new" / "estimates.json", stat, det) - _write_estimates(group / "det_exact" / "new" / "estimates.json", stat, det_exact) - _write_estimates(group / "det_exact_f64_result" / "new" / "estimates.json", stat, det_exact * 1.1) - _write_estimates(group / "det_exact_rounded_f64" / "new" / "estimates.json", stat, det_exact * 1.2) - _write_estimates(group / "solve_exact_f64_result" / "new" / "estimates.json", stat, det_exact * 1.3) - _write_estimates(group / "solve_exact_rounded_f64" / "new" / "estimates.json", stat, det_exact * 1.4) - - ns_group = criterion_dir / "exact_near_singular_3x3" - _write_estimates(ns_group / "det_sign_exact" / "new" / "estimates.json", stat, 12000.0) - _write_estimates(ns_group / "solve_exact_f64_result" / "new" / "estimates.json", stat, 51000.0) - _write_estimates(ns_group / "solve_exact_rounded_f64" / "new" / "estimates.json", stat, 52000.0) - - random_group = criterion_dir / "exact_random_corpus_d3" - _write_estimates(random_group / "det_exact" / "new" / "estimates.json", stat, 33000.0) - _write_estimates(random_group / "solve_exact_f64_result" / "new" / "estimates.json", stat, 54000.0) - _write_estimates(random_group / "solve_exact_rounded_f64" / "new" / "estimates.json", stat, 55000.0) - - rational_group = criterion_dir / "rational_input_d8" - for index, bench in enumerate(bench_compare._RATIONAL_INPUT_BENCHES, start=1): - _write_estimates(rational_group / bench / "new" / "estimates.json", stat, 100000.0 * index) - - -def _build_vs_linalg_tree(criterion_dir: Path, stat: str = "median") -> None: - """Create fake Criterion vs_linalg data with latest la-stack and baseline peer rows.""" - group = criterion_dir / "d2" - - _write_estimates(group / "la_stack_lu_solve" / "new" / "estimates.json", stat, 10.0) - _write_estimates(group / "la_stack_lu_solve" / "last" / "estimates.json", stat, 20.0) - _write_estimates(group / "nalgebra_lu_solve" / "last" / "estimates.json", stat, 30.0) - _write_estimates(group / "faer_lu_solve" / "last" / "estimates.json", stat, 40.0) - - _write_estimates(group / "la_stack_ldlt_solve" / "new" / "estimates.json", stat, 12.0) - _write_estimates(group / "la_stack_ldlt_solve" / "last" / "estimates.json", stat, 24.0) - _write_estimates(group / "nalgebra_cholesky_solve" / "last" / "estimates.json", stat, 36.0) - _write_estimates(group / "faer_ldlt_solve" / "last" / "estimates.json", stat, 48.0) - - # Present only in the baseline; release-signal comparison should not require - # latest third-party measurements. - _write_estimates(group / "nalgebra_lu_solve" / "new" / "estimates.json", stat, 31.0) - - -# --------------------------------------------------------------------------- -# Unit tests for formatting helpers -# --------------------------------------------------------------------------- - - -class TestFormatTime: - def test_nanoseconds(self) -> None: - assert bench_compare.format_time(42.5) == "42.5 ns" - - def test_microseconds(self) -> None: - assert bench_compare.format_time(1_500.0) == "1.50 µs" - - def test_milliseconds(self) -> None: - assert bench_compare.format_time(2_500_000.0) == "2.50 ms" - - -class TestFormatPct: - def test_ci_separated_point_change_is_not_bold(self) -> None: - assert bench_compare._format_pct(-10.0, "improvement") == "-10.0%" - - def test_inconclusive_point_improvement_is_not_bold(self) -> None: - assert bench_compare._format_pct(-10.0, "inconclusive") == "-10.0%" - - def test_small_change(self) -> None: - assert bench_compare._format_pct(0.5, "inconclusive") == "+0.5%" - - -# --------------------------------------------------------------------------- -# Group heading -# --------------------------------------------------------------------------- - - -class TestGroupHeading: - def test_dimension_group(self) -> None: - assert bench_compare._group_heading("exact_d3") == "D=3" - - def test_near_singular(self) -> None: - assert bench_compare._group_heading("exact_near_singular_3x3") == "Near-singular 3x3" - - def test_random_corpus(self) -> None: - assert bench_compare._group_heading("exact_random_corpus_d4") == "Random corpus D=4" - - def test_rational_input(self) -> None: - assert bench_compare._group_heading("rational_input_d8") == "Rational input D=8" - - def test_large_entries(self) -> None: - assert bench_compare._group_heading("exact_large_entries_3x3") == "Large entries 3x3" - - def test_hilbert_4x4(self) -> None: - assert bench_compare._group_heading("exact_hilbert_4x4") == "Hilbert 4x4" - - def test_hilbert_5x5(self) -> None: - assert bench_compare._group_heading("exact_hilbert_5x5") == "Hilbert 5x5" - - def test_unknown_passthrough(self) -> None: - assert bench_compare._group_heading("something_else") == "something_else" - - -def test_exact_registry_only_tracks_supported_direct_determinant_filters() -> None: - filter_benches = ["det_direct", "det_direct_with_errbound", "det_errbound"] - for dimension in (2, 3, 4): - benches = bench_compare.EXACT_GROUPS[f"exact_d{dimension}"] - start = benches.index("det_direct") - assert benches[start : start + len(filter_benches)] == filter_benches - assert not set(filter_benches) & set(bench_compare.EXACT_GROUPS["exact_d5"]) - - -def test_exact_registry_tracks_every_rational_input_release_dimension() -> None: - expected = [ - "det_sign_row_cleared_bareiss", - "det_row_cleared_bareiss", - "det_big_rational_gaussian", - "solve_row_cleared_bareiss", - "solve_big_rational_gaussian", - ] - assert [group for group in bench_compare.EXACT_GROUPS if group.startswith("rational_input_d")] == [ - f"rational_input_d{dimension}" for dimension in range(2, 9) - ] - assert all(bench_compare.EXACT_GROUPS[f"rational_input_d{dimension}"] == expected for dimension in range(2, 9)) - - -# --------------------------------------------------------------------------- -# read_estimate -# --------------------------------------------------------------------------- - - -def test_collect_results(tmp_path: Path) -> None: - _build_criterion_tree(tmp_path) - results = bench_compare._collect_results(tmp_path, "new", "median") - assert len(results) == 23 # Existing fixtures plus five rational-input rows. - groups = {r.group for r in results} - assert "exact_d2" in groups - assert "exact_d3" in groups - assert "exact_random_corpus_d3" in groups - assert "exact_near_singular_3x3" in groups - assert "rational_input_d8" in groups - - -def test_collect_results_empty_dir(tmp_path: Path) -> None: - results = bench_compare._collect_results(tmp_path, "new", "median") - assert results == [] - - -def test_collect_comparisons(tmp_path: Path) -> None: - _build_criterion_tree(tmp_path) - # Save the same data as a baseline - for d, det, det_exact in [(2, 2.0, 8000.0), (3, 10.0, 42000.0)]: - group = tmp_path / f"exact_d{d}" - _write_estimates(group / "det" / "v0.4.4" / "estimates.json", "median", det) - _write_estimates(group / "det_exact" / "v0.4.4" / "estimates.json", "median", det_exact) - for bench in ("det_exact_f64_result", "det_exact_rounded_f64"): - _write_estimates(group / bench / "v0.4.4" / "estimates.json", "median", det_exact * 1.1) - for bench in ("solve_exact_f64_result", "solve_exact_rounded_f64"): - _write_estimates(group / bench / "v0.4.4" / "estimates.json", "median", det_exact * 1.3) - random_group = tmp_path / "exact_random_corpus_d3" - _write_estimates(random_group / "det_exact" / "v0.4.4" / "estimates.json", "median", 66000.0) - for bench in ("solve_exact_f64_result", "solve_exact_rounded_f64"): - _write_estimates(random_group / bench / "v0.4.4" / "estimates.json", "median", 108000.0) - - collection = bench_compare._collect_comparisons(tmp_path, "v0.4.4", "median") - comparisons = collection.comparisons - assert len(comparisons) == 15 # fixed dimensions plus the stable random corpus - assert {c.group for c in comparisons} == { - "exact_d2", - "exact_d3", - "exact_random_corpus_d3", - } - for c in comparisons: - assert c.speedup == pytest.approx(c.baseline_ns / c.current_ns) - assert all(c.baseline_bench is None for c in comparisons) - - all_collection = bench_compare._collect_comparisons( - tmp_path, - "v0.4.4", - "median", - policy=bench_compare.ComparisonPolicy(scope="all-benches"), - ) - all_comparisons = all_collection.comparisons - assert len(all_comparisons) == 15 - random_comparisons = [c for c in all_comparisons if c.group == "exact_random_corpus_d3"] - assert {(c.bench, c.baseline_bench) for c in random_comparisons} == { - ("det_exact", None), - ("solve_exact_f64_result", None), - ("solve_exact_rounded_f64", None), - } - - -def test_collect_comparisons_missing_baseline(tmp_path: Path) -> None: - _build_criterion_tree(tmp_path) - collection = bench_compare._collect_comparisons(tmp_path, "v0.4.4", "median", suite="exact") - assert collection.comparisons == [] - det_gap = next(gap for gap in collection.gaps if gap.group == "exact_d2" and gap.bench == "det") - assert not det_gap.missing_current - assert det_gap.missing_baseline - - -def test_collect_comparisons_records_each_missing_side_in_registry_order(tmp_path: Path) -> None: - group = tmp_path / "exact_d2" - _write_estimates(group / "det" / "new" / "estimates.json", "median", 10.0) - _write_estimates(group / "det_direct" / "last" / "estimates.json", "median", 20.0) - - collection = bench_compare._collect_comparisons(tmp_path, "last", "median", suite="exact") - first_three = collection.gaps[:3] - assert [gap.bench for gap in first_three] == ["det", "det_direct", "det_direct_with_errbound"] - assert (first_three[0].missing_current, first_three[0].missing_baseline) == (False, True) - assert (first_three[1].missing_current, first_three[1].missing_baseline) == (True, False) - assert (first_three[2].missing_current, first_three[2].missing_baseline) == (True, True) - - -def test_pre_rational_adapter_retains_current_rational_rows_without_baselines(tmp_path: Path) -> None: - for group, bench in bench_compare._RATIONAL_INPUT_ROWS: - _write_estimates(tmp_path / group / bench / "new" / "estimates.json", "median", 10.0) - - policy = bench_compare.ComparisonPolicy(baseline_api_compatibility="la_stack_pre_rational_input_api") - collection = bench_compare._collect_comparisons( - tmp_path, - "v0.4.5", - "median", - suite="exact", - policy=policy, - ) - rows = bench_compare._unavailable_artifact_rows( - tmp_path, - baseline_name="v0.4.5", - stat="median", - suite="exact", - scope="release-signal", - policy=policy, - ) - - assert not [gap for gap in collection.gaps if gap.group.startswith("rational_input_d")] - assert len(rows) == len(bench_compare._RATIONAL_INPUT_ROWS) - assert all(row.coverage_status == "current-only" for row in rows) - assert all(row.baseline is None and row.current is not None for row in rows) - - -def test_pre_rational_historical_harness_excludes_rational_registry_rows(tmp_path: Path) -> None: - policy = bench_compare.ComparisonPolicy( - baseline_api_compatibility="la_stack_pre_rational_input_api", - shared_harness_rational_inputs=False, - ) - - collection = bench_compare._collect_comparisons( - tmp_path, - "v0.4.6", - "median", - suite="exact", - policy=policy, - ) - - assert not [gap for gap in collection.gaps if gap.group.startswith("rational_input_d")] - - -def test_collect_comparisons_reports_wholly_absent_selected_suite(tmp_path: Path) -> None: - group = tmp_path / "exact_d2" - _write_estimates(group / "det" / "new" / "estimates.json", "median", 10.0) - _write_estimates(group / "det" / "last" / "estimates.json", "median", 20.0) - - collection = bench_compare._collect_comparisons( - tmp_path, - "last", - "median", - suite="all", - policy=bench_compare.ComparisonPolicy(scope="all-benches"), - ) - - assert any(gap.suite == "vs_linalg" and gap.group == "(entire suite)" and gap.bench == "all selected rows" for gap in collection.gaps) - - -def test_release_signal_comparison_requires_every_canonical_vs_linalg_dimension(tmp_path: Path) -> None: - group = tmp_path / "d2" - _write_estimates(group / "la_stack_lu" / "new" / "estimates.json", "median", 10.0) - _write_estimates(group / "la_stack_lu" / "last" / "estimates.json", "median", 20.0) - - collection = bench_compare._collect_comparisons(tmp_path, "last", "median", suite="vs_linalg") - - missing_groups = {gap.group for gap in collection.gaps} - assert {"d3", "d4", "d5", "d8", "d16", "d32", "d64"} <= missing_groups - - -def test_collect_comparisons_classifies_from_criterion_intervals(tmp_path: Path) -> None: - group = tmp_path / "exact_d2" - fixtures = { - "det": ((100.0, 95.0, 105.0), (80.0, 75.0, 85.0), "improvement"), - "det_direct": ((100.0, 95.0, 105.0), (120.0, 115.0, 125.0), "regression"), - "det_exact": ((100.0, 90.0, 110.0), (95.0, 85.0, 105.0), "inconclusive"), - } - for bench, (baseline, current, _assessment) in fixtures.items(): - _write_estimates( - group / bench / "last" / "estimates.json", - "median", - baseline[0], - lower=baseline[1], - upper=baseline[2], - ) - _write_estimates( - group / bench / "new" / "estimates.json", - "median", - current[0], - lower=current[1], - upper=current[2], - ) - - no_ci_path = group / "det_exact_f64_result" / "new" / "estimates.json" - no_ci_path.parent.mkdir(parents=True, exist_ok=True) - no_ci_path.write_text(json.dumps({"median": {"point_estimate": 80.0}}), encoding="utf-8") - _write_estimates(group / "det_exact_f64_result" / "last" / "estimates.json", "median", 100.0) - - comparisons = bench_compare._collect_comparisons(tmp_path, "last", "median", suite="exact").comparisons - assessments = {comparison.bench: comparison.assessment for comparison in comparisons} - assert assessments == { - "det": "improvement", - "det_direct": "regression", - "det_exact": "inconclusive", - "det_exact_f64_result": "unknown", - } - - -@pytest.mark.parametrize("compatibility", [None, "unknown-adapter"]) -def test_d8_release_signal_rows_are_explicit_and_report_missing_baselines( - tmp_path: Path, - compatibility: str | None, -) -> None: - group = tmp_path / "d8" - for bench in bench_compare.VS_LINALG_D8_RELEASE_SIGNAL_BENCHES: - _write_estimates(group / bench / "new" / "estimates.json", "median", 10.0) - - collection = bench_compare._collect_comparisons( - tmp_path, - "last", - "median", - suite="vs_linalg", - policy=bench_compare.ComparisonPolicy(baseline_api_compatibility=compatibility), - ) - special_gaps = [gap for gap in collection.gaps if gap.bench in bench_compare.VS_LINALG_D8_RELEASE_SIGNAL_BENCHES] - assert [gap.bench for gap in special_gaps] == bench_compare.VS_LINALG_D8_RELEASE_SIGNAL_BENCHES - assert all(not gap.missing_current and gap.missing_baseline for gap in special_gaps) - - -def test_collect_vs_linalg_release_signal_uses_baseline_peer_context(tmp_path: Path) -> None: - _build_vs_linalg_tree(tmp_path) - collection = bench_compare._collect_comparisons(tmp_path, "last", "median", suite="vs_linalg") - comparisons = collection.comparisons - - assert [c.bench for c in comparisons] == ["la_stack_lu_solve", "la_stack_ldlt_solve"] - lu = comparisons[0] - assert lu.baseline_ns == 20.0 - assert lu.current_ns == 10.0 - assert lu.baseline_nalgebra_ns == 30.0 - assert lu.baseline_faer_ns == 40.0 - - ldlt = comparisons[1] - assert ldlt.baseline_nalgebra_ns == 36.0 - assert ldlt.baseline_faer_ns == 48.0 - - -@pytest.mark.parametrize("native_ns", [None, 5.0]) -def test_dot_peer_context_never_substitutes_legacy_scalar_loop(tmp_path: Path, native_ns: float | None) -> None: - group = tmp_path / "d2" - _write_estimates(group / "la_stack_dot/last/estimates.json", "median", 20.0) - _write_estimates(group / "la_stack_dot/new/estimates.json", "median", 10.0) - _write_estimates(group / "nalgebra_dot/last/estimates.json", "median", 8.0) - _write_estimates(group / "faer_dot/last/estimates.json", "median", 1.0) - if native_ns is not None: - _write_estimates(group / "faer_dot_native/last/estimates.json", "median", native_ns) - - collection = bench_compare._collect_comparisons(tmp_path, "last", "median", suite="vs_linalg") - - [dot] = collection.comparisons - assert dot.bench == "la_stack_dot" - assert dot.baseline_nalgebra_ns == 8.0 - assert dot.baseline_faer_ns == native_ns - - -def test_collect_vs_linalg_all_benches_includes_latest_peer_rows(tmp_path: Path) -> None: - _build_vs_linalg_tree(tmp_path) - collection = bench_compare._collect_comparisons( - tmp_path, - "last", - "median", - suite="vs_linalg", - policy=bench_compare.ComparisonPolicy(scope="all-benches"), - ) - comparisons = collection.comparisons - - assert [c.bench for c in comparisons] == [ - "la_stack_lu_solve", - "nalgebra_lu_solve", - "la_stack_ldlt_solve", - ] - assert {(gap.bench, gap.missing_current, gap.missing_baseline) for gap in collection.gaps} == { - ("faer_lu_solve", True, False), - ("nalgebra_cholesky_solve", True, False), - ("faer_ldlt_solve", True, False), - } - - -# --------------------------------------------------------------------------- -# Table generation -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize(("current_ns", "change"), [(5.0, "-50.0%"), (10.0, "+0.0%"), (20.0, "+100.0%")]) -def test_comparison_table_preserves_signed_change(current_ns: float, change: str) -> None: - """The report uses signed elapsed-time change, including positive zero.""" - comparison = bench_compare.Comparison( - suite="exact", - group="exact_d2", - bench="det", - baseline=bench_compare.Estimate(10.0, None, None), - current=bench_compare.Estimate(current_ns, None, None), - assessment="unknown", - ) - - table = bench_compare.comparison_tables([comparison], "last") - - assert f"| {change} |" in table - - -def test_snapshot_uses_one_table_per_suite_with_case_column(tmp_path: Path) -> None: - _build_criterion_tree(tmp_path) - results = bench_compare._collect_results(tmp_path, "new", "median") - tables = bench_compare._snapshot_tables(results, "median") - assert tables.count("| Case | Benchmark | Median | Criterion CI |") == 1 - assert "| D=2 |" in tables - assert "| D=3 |" in tables - assert "| Random corpus D=3 |" in tables - assert "| Near-singular 3x3 |" in tables - assert "| Rational input D=8 |" in tables - - -def test_snapshot_tables_uses_stat_label(tmp_path: Path) -> None: - _build_criterion_tree(tmp_path, stat="mean") - results = bench_compare._collect_results(tmp_path, "new", "mean") - tables = bench_compare._snapshot_tables(results, "mean") - assert "| Case | Benchmark | Mean | Criterion CI |" in tables - assert "Median" not in tables - - -def test_comparison_uses_one_table_per_suite_with_case_column(tmp_path: Path) -> None: - _build_criterion_tree(tmp_path) - for d, det, det_exact in [(2, 2.0, 8000.0), (3, 10.0, 42000.0)]: - group = tmp_path / f"exact_d{d}" - _write_estimates(group / "det" / "v0.4.4" / "estimates.json", "median", det) - _write_estimates(group / "det_exact" / "v0.4.4" / "estimates.json", "median", det_exact) - for bench in ("det_exact_f64_result", "det_exact_rounded_f64"): - _write_estimates(group / bench / "v0.4.4" / "estimates.json", "median", det_exact * 1.1) - for bench in ("solve_exact_f64_result", "solve_exact_rounded_f64"): - _write_estimates(group / bench / "v0.4.4" / "estimates.json", "median", det_exact * 1.3) - - comparisons = bench_compare._collect_comparisons(tmp_path, "v0.4.4", "median").comparisons - tables = bench_compare.comparison_tables(comparisons, "v0.4.4") - assert tables.count("| Case | Benchmark | v0.4.4 (point + CI)") == 1 - assert "| D=2 |" in tables - assert "| D=3 |" in tables - assert "| det_exact_rounded_f64 |" in tables - assert "| solve_exact_f64_result |" in tables - - -def test_comparison_tables_include_vs_linalg_peer_context(tmp_path: Path) -> None: - _build_vs_linalg_tree(tmp_path) - comparisons = bench_compare._collect_comparisons(tmp_path, "last", "median", suite="vs_linalg").comparisons - tables = bench_compare.comparison_tables(comparisons, "last") - - assert ( - "| Case | Benchmark | last (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | " - "Point-estimate ratio | last nalgebra | last faer |" in tables - ) - assert ( - "| D=2 | la_stack_lu_solve | 20.0 ns [18.0 ns, 22.0 ns] | 10.0 ns [9.0 ns, 11.0 ns] | -50.0% | " - "faster point estimate; marginal CIs separated | 2.00x | " - "30.0 ns [27.0 ns, 33.0 ns] | 40.0 ns [36.0 ns, 44.0 ns] |" in tables - ) - assert ( - "| D=2 | la_stack_ldlt_solve | 24.0 ns [21.6 ns, 26.4 ns] | 12.0 ns [10.8 ns, 13.2 ns] | -50.0% | " - "faster point estimate; marginal CIs separated | 2.00x | " - "36.0 ns [32.4 ns, 39.6 ns] | 48.0 ns [43.2 ns, 52.8 ns] |" in tables - ) - - -def test_coverage_table_makes_missing_samples_explicit(tmp_path: Path) -> None: - group = tmp_path / "exact_d2" - _write_estimates(group / "det" / "new" / "estimates.json", "median", 10.0) - _write_estimates(group / "det_direct" / "last" / "estimates.json", "median", 20.0) - collection = bench_compare._collect_comparisons(tmp_path, "last", "median", suite="exact") - - table = bench_compare._coverage_table(collection.gaps, "last") - assert "## Incomplete Comparison Coverage" in table - assert "| Exact arithmetic | exact_d2 | det | baseline (`last`) |" in table - assert "| Exact arithmetic | exact_d2 | det_direct | current (`new`) |" in table - assert "| Exact arithmetic | exact_d2 | det_exact | current (`new`), baseline (`last`) |" in table - - -# --------------------------------------------------------------------------- -# Harness provenance -# --------------------------------------------------------------------------- - - -def test_read_harness_provenance_validates_shared_harness_metadata(tmp_path: Path) -> None: - _write_harness_provenance(tmp_path) - - provenance = _read_harness_provenance(tmp_path) - - assert provenance == bench_compare.HarnessProvenance( - schema=1, - mode="shared-current-harness", - sha256="a" * 64, - baseline="v0.4.5", - ) - policy = bench_compare._comparison_policy("release-signal", provenance) - assert policy == bench_compare.ComparisonPolicy( - scope="release-signal", - shared_harness_rational_inputs=False, - ) - - collection = bench_compare._collect_comparisons( - tmp_path, - "v0.4.5", - "median", - suite="exact", - policy=policy, - ) - assert not [gap for gap in collection.gaps if gap.group.startswith("rational_input_d")] - - -def test_read_harness_provenance_is_optional(tmp_path: Path) -> None: - assert _read_harness_provenance(tmp_path) is None - - -def test_read_schema2_provenance_records_versions_dirty_source_and_both_gates(tmp_path: Path) -> None: - tmp_path.mkdir(parents=True, exist_ok=True) - (tmp_path / ".la-stack-benchmark-harness.json").write_text( - json.dumps(_schema2_provenance_data()), - encoding="utf-8", - ) - - provenance = _read_harness_provenance(tmp_path) - - assert provenance is not None - assert provenance.schema == 2 - assert provenance.criterion is not None - assert provenance.criterion.criterion_version == "0.7.0" - markdown = bench_compare._provenance_markdown(provenance) - rendered = "\n".join(markdown) - assert "Criterion dependency version: `0.7.0`" in rendered - assert "Current Git clean: `false`" in rendered - assert "Validated baseline revision: `baseline-commit`" in rendered - assert "Baseline API compatibility: `la_stack_pre_rational_input_api`" in rendered - assert "rational_input_d{2..8}/*" in rendered - assert "**Publication and validation environment**:" in rendered - assert all(len(line) <= 160 for line in markdown) - assert bench_compare._comparison_policy("release-signal", provenance) == bench_compare.ComparisonPolicy( - scope="release-signal", - baseline_api_compatibility="la_stack_pre_rational_input_api", - ) - assert provenance.measurement is not None - with pytest.raises(TypeError): - cast("dict[str, object]", provenance.measurement)["status"] = "unavailable" - - -def test_historical_asset_provenance_uses_mode_appropriate_gate_wording(tmp_path: Path) -> None: - data = _schema2_provenance_data() - data["mode"] = "historical-assets" - data["measurement"] = { - "status": "unavailable", - "reason": "historical release assets do not record the timing environment", - } - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - provenance = _read_harness_provenance(tmp_path) - - assert provenance is not None - rendered = "\n".join(bench_compare._provenance_markdown(provenance)) - assert "historical release assets do not record the timing environment" in rendered - assert "passed for both referenced source revisions during publication" in rendered - assert "separately from the historical timing measurements" in rendered - assert "shared current fixture harness" not in rendered - assert "both samples under one shared current harness" not in rendered - - -def test_read_schema2_provenance_requires_criterion_version(tmp_path: Path) -> None: - data = _schema2_provenance_data() - criterion = data["criterion"] - assert isinstance(criterion, dict) - del cast("dict[str, object]", criterion)["criterion_version"] - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - with pytest.raises(ValueError, match="criterion_version"): - _read_harness_provenance(tmp_path) - - -def test_read_schema2_provenance_rejects_recorded_measurement_without_cpu_model(tmp_path: Path) -> None: - data = _schema2_provenance_data() - measurement = data["measurement"] - publication = data["publication"] - assert isinstance(measurement, dict) - assert isinstance(publication, dict) - cast("dict[str, object]", measurement)["cpu"] = "unavailable" - cast("dict[str, object]", publication)["cpu"] = "unavailable" - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - with pytest.raises(ValueError, match="requires an identified CPU model"): - _read_harness_provenance(tmp_path) - - -@pytest.mark.parametrize( - ("field", "value"), - [ - ("suite", "exact"), - ("scope", "all-benches"), - ("statistic", "mean"), - ("sample", "v0.4.5"), - ], -) -def test_read_schema2_provenance_binds_criterion_settings_to_request( - tmp_path: Path, - field: str, - value: str, -) -> None: - data = _schema2_provenance_data() - criterion = data["criterion"] - assert isinstance(criterion, dict) - cast("dict[str, object]", criterion)[field] = value - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - with pytest.raises(ValueError, match=rf"criterion\.{field}.*does not match requested value"): - _read_harness_provenance(tmp_path) - - -def test_read_schema2_provenance_rejects_pre_rational_adapter_for_newer_baseline(tmp_path: Path) -> None: - data = _schema2_provenance_data() - data["baseline"] = "v0.4.6" - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - with pytest.raises( - ValueError, - match=r"must be 'none' for baseline 'v0\.4\.6'", - ): - _read_harness_provenance(tmp_path, baseline="v0.4.6") - - -def test_read_schema2_provenance_accepts_pre_rational_adapter_for_v045(tmp_path: Path) -> None: - data = _schema2_provenance_data() - data["baseline"] = "v0.4.5" - measurement = data["measurement"] - validation = data["validation"] - assert isinstance(measurement, dict) - assert isinstance(validation, dict) - cast("dict[str, object]", measurement)["baseline_api_compatibility"] = "la_stack_pre_rational_input_api" - cast("dict[str, object]", validation)["baseline_api_compatibility"] = "la_stack_pre_rational_input_api" - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - provenance = _read_harness_provenance(tmp_path, baseline="v0.4.5") - - assert provenance is not None - assert provenance.validation is not None - assert provenance.validation["baseline_api_compatibility"] == "la_stack_pre_rational_input_api" - - -def test_read_schema2_provenance_rejects_mode_status_contradiction(tmp_path: Path) -> None: - data = _schema2_provenance_data() - data["mode"] = "historical-assets" - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - with pytest.raises(ValueError, match="requires shared-current-harness mode"): - _read_harness_provenance(tmp_path) - - -def test_read_schema2_provenance_rejects_revision_contradiction(tmp_path: Path) -> None: - data = _schema2_provenance_data() - publication = data["publication"] - assert isinstance(publication, dict) - cast("dict[str, object]", publication)["commit"] = "different-commit" - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - with pytest.raises(ValueError, match=r"publication\.commit.*validation\.current_commit"): - _read_harness_provenance(tmp_path) - - -def test_read_harness_provenance_rejects_different_requested_baseline(tmp_path: Path) -> None: - _write_harness_provenance(tmp_path, baseline="v0.4.5") - - with pytest.raises(ValueError, match="does not match requested Criterion baseline 'last'"): - _read_harness_provenance(tmp_path, baseline="last") - - -@pytest.mark.parametrize( - ("field", "value", "message"), - [ - ("schema", True, "unsupported or missing schema"), - ("schema", 1.0, "unsupported or missing schema"), - ("schema", 2.0, "unsupported or missing schema"), - ("schema", 3, "unsupported or missing schema"), - ("mode", "independent-harnesses", "unsupported or missing mode"), - ("sha256", "not-a-digest", "invalid or missing sha256"), - ("baseline", "", "invalid or missing baseline"), - ], -) -def test_read_harness_provenance_rejects_malformed_fields( - tmp_path: Path, - field: str, - value: object, - message: str, -) -> None: - data: dict[str, object] = { - "schema": 1, - "mode": "shared-current-harness", - "sha256": "a" * 64, - "baseline": "v0.4.5", - } - data[field] = value - tmp_path.mkdir(parents=True, exist_ok=True) - (tmp_path / ".la-stack-benchmark-harness.json").write_text(json.dumps(data), encoding="utf-8") - - with pytest.raises((TypeError, ValueError), match=message): - _read_harness_provenance(tmp_path) - - -# --------------------------------------------------------------------------- -# CLI / main -# --------------------------------------------------------------------------- - - -def test_main_snapshot_writes_output(tmp_path: Path) -> None: - criterion_dir = tmp_path / "criterion" - _build_criterion_tree(criterion_dir) - output = tmp_path / "performance.md" - - rc = bench_compare.main( - [ - "--snapshot", - "--suite", - "exact", - "--scope", - "all-benches", - "--criterion-dir", - str(criterion_dir), - "--output", - str(output), - ] - ) - assert rc == 0 - assert output.exists() - - text = output.read_text(encoding="utf-8") - assert "| D=2 |" in text - assert "| Random corpus D=3 |" in text - assert "| Near-singular 3x3 |" in text - assert "just performance-local" in text - assert "just performance-release" in text - assert "just performance-doc" in text - assert "just performance-github-assets" in text - assert "just performance-release " in text - assert "untracked files are excluded" in text - assert "performance-local` writes `performance.md` plus retained `performance.csv`" in text - assert "performance-doc` consumes the retained pair from either workflow" in text - assert "performance-local` followed by `performance-doc` is equivalent" in text - assert "Older curated release-to-release reports are archived in `docs/archive/performance/`." in text - assert "git checkout" not in text - assert text.endswith(bench_compare.HOW_TO_UPDATE_SECTION) - - -def test_main_no_criterion_dir(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - rc = bench_compare.main(["--criterion-dir", str(tmp_path / "nonexistent"), "--output", str(tmp_path / "out.md")]) - assert rc == 2 - assert "No Criterion results" in capsys.readouterr().err - - -def test_repo_root_resolution_uses_working_checkout_for_installed_entrypoint(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - nested = tmp_path / "nested" - nested.mkdir() - (tmp_path / "Cargo.toml").write_text("[package]\n", encoding="utf-8") - monkeypatch.chdir(nested) - - assert bench_compare._repo_root() == tmp_path - - -def test_main_comparison_no_baseline(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - criterion_dir = tmp_path / "criterion" - _build_criterion_tree(criterion_dir) - - rc = bench_compare.main(["v0.4.4", "--criterion-dir", str(criterion_dir), "--output", str(tmp_path / "out.md")]) - assert rc == 2 - assert "No comparison data" in capsys.readouterr().err - - -def test_main_comparison_refuses_incomplete_coverage_before_writing(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - criterion_dir = tmp_path / "criterion" - group = criterion_dir / "exact_d2" - _write_estimates(group / "det" / "new" / "estimates.json", "median", 10.0) - _write_estimates(group / "det" / "v0.4.5" / "estimates.json", "median", 20.0) - _write_harness_provenance(criterion_dir) - output = tmp_path / "report.md" - - rc = bench_compare.main(["v0.4.5", "--suite", "exact", "--criterion-dir", str(criterion_dir), "--output", str(output)]) - - assert rc == 2 - assert not output.exists() - error = capsys.readouterr().err - assert "Incomplete benchmark coverage" in error - assert "## Incomplete Comparison Coverage" in error - - -@pytest.mark.parametrize("point", [0.0, "10.0", True]) -def test_main_rejects_invalid_timing_without_writing_or_traceback( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], - point: object, -) -> None: - criterion_dir = tmp_path / "criterion" - group = criterion_dir / "exact_d2" - current = group / "det/new/estimates.json" - current.parent.mkdir(parents=True) - current.write_text(json.dumps({"median": {"point_estimate": point}}), encoding="utf-8") - _write_estimates(group / "det" / "last" / "estimates.json", "median", 10.0) - output = tmp_path / "report.md" - - rc = bench_compare.main(["last", "--suite", "exact", "--criterion-dir", str(criterion_dir), "--output", str(output)]) - - assert rc == 2 - assert "Invalid Criterion estimate data" in capsys.readouterr().err - assert not output.exists() - - -@pytest.mark.parametrize(("baseline", "current"), [(1e-300, 1e300), (1e300, 1e-300)]) -def test_main_rejects_unrepresentable_comparison_without_changing_report( - tmp_path: Path, capsys: pytest.CaptureFixture[str], baseline: float, current: float -) -> None: - criterion_dir = tmp_path / "criterion" - group = criterion_dir / "d2/la_stack_lu_solve" - _write_estimates(group / "new/estimates.json", "median", current) - _write_estimates(group / "last/estimates.json", "median", baseline) - output = tmp_path / "report.md" - output.write_bytes(b"prior report\n") - - rc = bench_compare.main(["last", "--suite", "vs_linalg", "--scope", "all-benches", "--criterion-dir", str(criterion_dir), "--output", str(output)]) - - assert rc == 2 - captured = capsys.readouterr() - assert "Invalid Criterion comparison data" in captured.err - assert not captured.out - assert output.read_bytes() == b"prior report\n" - - -@pytest.mark.parametrize("kind", ["directory", "symlink"]) -def test_main_snapshot_rejects_invalid_output_without_traceback(tmp_path: Path, capsys: pytest.CaptureFixture[str], kind: str) -> None: - criterion_dir = tmp_path / "criterion" - _build_criterion_tree(criterion_dir) - output = tmp_path / "report.md" - previous = tmp_path / "keep.txt" - previous.write_bytes(b"keep\n") - if kind == "directory": - output.mkdir() - else: - try: - output.symlink_to(previous) - except OSError as exc: - pytest.skip(f"symlinks unavailable: {exc}") - - rc = bench_compare.main(["--snapshot", "--suite", "exact", "--scope", "all-benches", "--criterion-dir", str(criterion_dir), "--output", str(output)]) - - assert rc == 2 - captured = capsys.readouterr() - assert "Could not write benchmark report" in captured.err - assert not captured.out - assert previous.read_bytes() == b"keep\n" - - -def test_main_rejects_overflowing_timing_without_writing_or_traceback( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - criterion_dir = tmp_path / "criterion" - current = criterion_dir / "exact_d2" / "det" / "new" / "estimates.json" - current.parent.mkdir(parents=True) - current.write_text(json.dumps({"median": {"point_estimate": _OVERFLOWING_TIMING}}), encoding="utf-8") - _write_estimates(criterion_dir / "exact_d2" / "det" / "last" / "estimates.json", "median", 10.0) - output = tmp_path / "report.md" - - rc = bench_compare.main(["last", "--suite", "exact", "--criterion-dir", str(criterion_dir), "--output", str(output)]) - - assert rc == 2 - assert "Invalid Criterion estimate data" in capsys.readouterr().err - assert not output.exists() - - -@pytest.mark.parametrize("invalid_paths", ["same-pair", "non-adjacent", "markdown-alias"]) -def test_main_rejects_invalid_artifact_paths_without_writing( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], - invalid_paths: str, -) -> None: - output = tmp_path / "report.md" - csv_output = tmp_path / "artifacts" / "performance.csv" - provenance_output = tmp_path / "artifacts" / "performance.provenance.json" - if invalid_paths == "same-pair": - provenance_output = csv_output - elif invalid_paths == "non-adjacent": - provenance_output = tmp_path / "other" / "performance.provenance.json" - else: - output = csv_output - - paths = {output, csv_output, provenance_output} - for path in paths: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(f"original {path.name}\n".encode()) - before = {path: path.read_bytes() for path in paths} - - rc = bench_compare.main( - [ - "v0.4.5", - "--repo-root", - str(tmp_path), - "--output", - str(output), - "--csv-output", - str(csv_output), - "--provenance-output", - str(provenance_output), - ] - ) - - assert rc == 2 - assert "Invalid release-performance artifact paths" in capsys.readouterr().err - assert {path: path.read_bytes() for path in paths} == before - - -@pytest.mark.parametrize( - ("invalid_options", "expected_error"), - [ - ("csv-only", "--csv-output and --provenance-output must be provided together"), - ("provenance-only", "--csv-output and --provenance-output must be provided together"), - ("snapshot", "release-performance artifacts require a median baseline comparison"), - ("mean", "release-performance artifacts require a median baseline comparison"), - ], -) -def test_main_rejects_invalid_artifact_option_combinations( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], - invalid_options: str, - expected_error: str, -) -> None: - csv_output = tmp_path / "performance.csv" - provenance_output = tmp_path / "performance.provenance.json" - artifact_args = ["--csv-output", str(csv_output), "--provenance-output", str(provenance_output)] - if invalid_options == "csv-only": - artifact_args = artifact_args[:2] - elif invalid_options == "provenance-only": - artifact_args = artifact_args[2:] - elif invalid_options == "snapshot": - artifact_args.append("--snapshot") - else: - artifact_args.extend(["--stat", "mean"]) - - rc = bench_compare.main(["--repo-root", str(tmp_path), *artifact_args]) - - assert rc == 2 - assert expected_error in capsys.readouterr().err - assert not csv_output.exists() - assert not provenance_output.exists() - - -def test_main_preserves_artifact_exception_group_diagnostics( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - criterion_dir = tmp_path / "criterion" - criterion_dir.mkdir() - csv_output = tmp_path / "performance.csv" - provenance_output = tmp_path / "performance.provenance.json" - output = tmp_path / "performance.md" - - monkeypatch.setattr(bench_compare, "_read_harness_provenance", lambda *_args, **_kwargs: None) - monkeypatch.setattr( - bench_compare, - "_collect_comparisons", - lambda *_args, **_kwargs: SimpleNamespace(comparisons=[object()], gaps=[]), - ) - monkeypatch.setattr(bench_compare, "comparison_tables", lambda *_args, **_kwargs: "table") - - def fail_publication(*_args: object, **_kwargs: object) -> None: - message = "artifact publication and rollback failed" - raise ExceptionGroup( - message, - [OSError("could not publish performance.csv"), OSError("could not restore provenance")], - ) - - monkeypatch.setattr(bench_compare, "_write_and_render_artifacts", fail_publication) - - status = bench_compare.main( - [ - "last", - "--repo-root", - str(tmp_path), - "--criterion-dir", - str(criterion_dir), - "--output", - str(output), - "--csv-output", - str(csv_output), - "--provenance-output", - str(provenance_output), - ] - ) - - assert status == 2 - captured = capsys.readouterr() - assert "artifact publication and rollback failed (2 sub-exceptions)" in captured.err - assert "could not publish performance.csv" in captured.err - assert "could not restore provenance" in captured.err - - -def test_markdown_failure_rolls_back_release_artifact_pair(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - paths = bench_compare.ArtifactPaths( - csv=tmp_path / "performance.csv", - provenance=tmp_path / "performance.provenance.json", - ) - output = tmp_path / "performance.md" - paths.csv.write_bytes(b"old csv\n") - paths.provenance.write_bytes(b"old provenance\n") - output.write_text("old markdown\n", encoding="utf-8") - timing = bench_compare.TimingEstimate(median_ns=10.0, ci_lower_ns=9.0, ci_upper_ns=11.0) - bundle = bench_compare.PerformanceBundle( - context=bench_compare.ArtifactContext( - release=bench_compare.ReleasePair(current="v0.4.6", baseline="v0.4.5"), - statistic="median", - suite="all", - scope="release-signal", - source=bench_compare.ReportSource( - version="0.4.6", - commit="current-commit", - ref="HEAD", - revision_timestamp="2026-08-04 12:00:00 UTC", - ), - benchmark_provenance=_schema2_provenance_data(), - ), - rows=( - bench_compare.PerformanceRow( - suite="exact", - scope="release-signal", - benchmark_id="exact_d2/det_exact", - group="exact_d2", - benchmark="det_exact", - baseline_benchmark="det_exact", - coverage_status="comparable", - coverage_note="", - baseline=timing, - current=timing, - ), - ), - ) - monkeypatch.setattr(bench_compare, "_release_artifact_bundle", lambda **_kwargs: bundle) - - real_replace = Path.replace - failed = False - - def fail_markdown(source: Path, destination: Path) -> Path: - nonlocal failed - if destination == output and not failed: - failed = True - msg = "simulated Markdown publication failure" - raise OSError(msg) - return real_replace(source, destination) - - monkeypatch.setattr(Path, "replace", fail_markdown) - - with pytest.raises(OSError, match="simulated Markdown publication failure"): - bench_compare._write_and_render_artifacts( - paths, - output_path=output, - root=tmp_path, - criterion_dir=tmp_path, - settings=bench_compare.ReportSettings( - baseline_name="v0.4.5", - stat="median", - suite="all", - scope="release-signal", - ), - collection=bench_compare.ComparisonCollection(comparisons=[], gaps=[]), - ) - - assert paths.csv.read_bytes() == b"old csv\n" - assert paths.provenance.read_bytes() == b"old provenance\n" - assert output.read_text(encoding="utf-8") == "old markdown\n" - - -def test_main_v045_comparison_publishes_current_only_rational_rows( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - criterion_dir = tmp_path / "criterion" - for group, benches in bench_compare.EXACT_GROUPS.items(): - for bench in benches: - _write_estimates( - criterion_dir / group / bench / "new" / "estimates.json", - "median", - 10.0, - ) - if (group, bench) not in bench_compare._RATIONAL_INPUT_ROWS: - _write_estimates( - criterion_dir / group / bench / "v0.4.5" / "estimates.json", - "median", - 20.0, - ) - - provenance = _schema2_provenance_data() - provenance["baseline"] = "v0.4.5" - provenance["current"] = "v0.4.6" - criterion = provenance["criterion"] - measurement = provenance["measurement"] - validation = provenance["validation"] - assert isinstance(criterion, dict) - assert isinstance(measurement, dict) - assert isinstance(validation, dict) - cast("dict[str, object]", criterion)["suite"] = "exact" - cast("dict[str, object]", measurement)["baseline_api_compatibility"] = "la_stack_pre_rational_input_api" - cast("dict[str, object]", validation)["baseline_api_compatibility"] = "la_stack_pre_rational_input_api" - (criterion_dir / ".la-stack-benchmark-harness.json").write_text(json.dumps(provenance), encoding="utf-8") - output = tmp_path / "report.md" - artifact_paths = bench_compare.ArtifactPaths( - csv=tmp_path / "performance.csv", - provenance=tmp_path / "performance.provenance.json", - ) - monkeypatch.setattr( - bench_compare, - "_report_source", - lambda _root: bench_compare.ReportSource( - version="0.4.6", - commit="current-commit", - ref="test", - revision_timestamp="2026-09-02 12:00:00 UTC", - ), - ) - - rc = bench_compare.main( - [ - "v0.4.5", - "--suite", - "exact", - "--criterion-dir", - str(criterion_dir), - "--csv-output", - str(artifact_paths.csv), - "--provenance-output", - str(artifact_paths.provenance), - "--output", - str(output), - ] - ) - - assert rc == 0 - rendered = output.read_text(encoding="utf-8") - assert rendered == bench_compare.render_release_artifacts(artifact_paths) - assert "rational_input_d2/det_sign_row_cleared_bareiss" in rendered - assert "rational_input_d8/solve_big_rational_gaussian" in rendered - assert rendered.count("current-only") == len(bench_compare._RATIONAL_INPUT_ROWS) - assert "no correctness-compatible benchmark row" in rendered - - -def test_generate_markdown_labels_absent_provenance_unavailable(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(bench_compare, "_get_git_source_date", lambda _root: "2026-06-01 12:34:56 UTC") - report = bench_compare._generate_markdown( - tmp_path, - "tables", - bench_compare.ReportSettings( - baseline_name="last", - stat="median", - suite="exact", - scope="release-signal", - ), - ) - - assert "**Reproducibility provenance**: unavailable" in report - assert "CPU, OS, rustc, commit, dependency lock" in report - assert "performance-improvement claim" in report - assert "**Source revision timestamp**: 2026-06-01 12:34:56 UTC (deterministic report metadata; not the benchmark measurement time)" in report - assert "**Benchmark measurement timestamp**: not recorded by Criterion" in report - - -def test_git_source_date_is_normalized_to_unambiguous_utc(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr( - bench_compare, - "run_command", - lambda *_args, **_kwargs: subprocess.CompletedProcess([], 0, stdout="2026-06-01T05:34:56-07:00\n"), - ) - - assert bench_compare._get_git_source_date(tmp_path) == "2026-06-01 12:34:56 UTC" - - -def test_main_rejects_malformed_harness_provenance(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - criterion_dir = tmp_path / "criterion" - criterion_dir.mkdir() - (criterion_dir / ".la-stack-benchmark-harness.json").write_text("{not json", encoding="utf-8") - - rc = bench_compare.main(["last", "--criterion-dir", str(criterion_dir), "--output", str(tmp_path / "report.md")]) - - assert rc == 2 - assert "Invalid benchmark harness provenance" in capsys.readouterr().err - - -def test_main_rejects_harness_provenance_for_a_different_baseline( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - criterion_dir = tmp_path / "criterion" - group = criterion_dir / "exact_d2" - _write_estimates(group / "det" / "new" / "estimates.json", "median", 10.0) - _write_estimates(group / "det" / "last" / "estimates.json", "median", 20.0) - _write_harness_provenance(criterion_dir, baseline="v0.4.5") - output = tmp_path / "report.md" - - rc = bench_compare.main(["last", "--suite", "exact", "--criterion-dir", str(criterion_dir), "--output", str(output)]) - - assert rc == 2 - assert "does not match requested Criterion baseline 'last'" in capsys.readouterr().err - assert not output.exists() diff --git a/scripts/tests/test_changelog_integration.py b/scripts/tests/test_changelog_integration.py index 1c5cd9c..af6838b 100644 --- a/scripts/tests/test_changelog_integration.py +++ b/scripts/tests/test_changelog_integration.py @@ -17,18 +17,20 @@ def test_published_package_and_shared_policy() -> None: - """Both dependency owners and the registry lock select the published package.""" + """The dependency-only environment and registry lock select the published package.""" manifest = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) - assert manifest["project"]["dependencies"] == ["research-repo-tools==0.1.8"] - assert manifest["dependency-groups"]["tooling"] == ["research-repo-tools==0.1.8"] + assert manifest["project"]["dependencies"] == [] + assert manifest["tool"]["uv"]["package"] is False + assert "build-system" not in manifest + assert manifest["dependency-groups"]["tooling"] == ["research-repo-tools==0.1.9"] assert {"include-group": "tooling"} in manifest["dependency-groups"]["dev"] - assert version("research-repo-tools") == "0.1.8" + assert version("research-repo-tools") == "0.1.9" lock = tomllib.loads((ROOT / "uv.lock").read_text(encoding="utf-8")) package = next(item for item in lock["package"] if item["name"] == "research-repo-tools") - assert package["version"] == "0.1.8" + assert package["version"] == "0.1.9" assert package["source"] == {"registry": "https://pypi.org/simple"} - assert package["wheels"][0]["hash"] == "sha256:9db45f09a33cac4ff32ddf010d30523e295f607e24c680bcfc3d3d9786991293" - assert package["sdist"]["hash"] == "sha256:bc855aa930ee04301dceebc8340a22576bb25712e322b51d7afaf20f5fab1ba0" + assert package["wheels"][0]["hash"] == "sha256:c4805004e9a857c072e7b735b033dd13ad256fb283c6e1a4adca0aa59cb67540" + assert package["sdist"]["hash"] == "sha256:7bfb16d7a38e8b3cecd30ce526cf808b377b4480ebd512740198a0361c3d027b" assert manifest["tool"]["research-repo-tools"]["changelog"] == { "owner": "acgetchell", "repository": "la-stack", diff --git a/scripts/tests/test_criterion_dim_plot.py b/scripts/tests/test_criterion_dim_plot.py deleted file mode 100644 index f96aa06..0000000 --- a/scripts/tests/test_criterion_dim_plot.py +++ /dev/null @@ -1,1141 +0,0 @@ -"""Tests for Criterion dimension-report generation and README updates.""" - -import hashlib -import json -import re -import subprocess -import tomllib -from dataclasses import replace -from pathlib import Path -from types import SimpleNamespace -from typing import cast - -import pytest - -import criterion_dim_plot -from performance_artifacts import ( - ArtifactContext, - ArtifactPaths, - PerformanceBundle, - PerformanceRow, - ReleasePair, - ReportSource, - TimingEstimate, - write_bundle, -) - -_OVERFLOWING_TIMING = 10**400 - - -def _toml_dependency_version(data: dict[str, object], name: str) -> str | None: - for section in ("dependencies", "dev-dependencies", "build-dependencies"): - table = data.get(section) - if not isinstance(table, dict): - continue - table_dict = cast("dict[str, object]", table) - value = table_dict.get(name) - if value is None: - continue - if isinstance(value, str): - return value - if isinstance(value, dict): - value_dict = cast("dict[str, object]", value) - version = value_dict.get("version") - if isinstance(version, str): - return version - return None - - -def test_detect_versions_matches_cargo_toml() -> None: - root = criterion_dim_plot._repo_root() - cargo_toml = root / "Cargo.toml" - - data = tomllib.loads(cargo_toml.read_text(encoding="utf-8")) - - package_version: str | None = None - package = data.get("package") - if isinstance(package, dict): - version = package.get("version") - if isinstance(version, str): - package_version = version - - expected_la = package_version or "unknown" - comparison = tomllib.loads((root / "benches/comparison/Cargo.toml").read_text(encoding="utf-8")) - expected_na = _toml_dependency_version(comparison, "nalgebra") or "unknown" - expected_fa = _toml_dependency_version(comparison, "faer") or "unknown" - - versions = criterion_dim_plot._detect_versions(root) - - assert versions["la-stack"] == expected_la - assert versions["nalgebra"] == expected_na - assert versions["faer"] == expected_fa - - -def test_readme_table_markers_are_stable() -> None: - begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - assert begin == "" - assert end == "" - - -def test_markdown_table_formats_values_and_pct() -> None: - rows = [ - criterion_dim_plot.Row( - dim=2, - la_time=50.0, - la_lo=45.0, - la_hi=55.0, - na_time=100.0, - na_lo=90.0, - na_hi=110.0, - fa_time=200.0, - fa_lo=180.0, - fa_hi=220.0, - ), # +50.0% vs na, +75.0% vs fa - criterion_dim_plot.Row( - dim=64, - la_time=1_000.0, - la_lo=900.0, - la_hi=1_100.0, - na_time=900.0, - na_lo=800.0, - na_hi=1_000.0, - fa_time=800.0, - fa_lo=700.0, - fa_hi=900.0, - ), # -11.1% vs na, -25.0% vs fa - ] - - table = criterion_dim_plot._markdown_table(rows, stat="median") - - assert ( - "| D | la-stack median (ns) | nalgebra median (ns) | faer median (ns) | reduction vs nalgebra (point est.) | reduction vs faer (point est.) |" in table - ) - assert "| 2 | 50.000 | 100.000 | 200.000 | +50.0% | +75.0% |" in table - # thousand separator and sign - assert "| 64 | 1,000.000 | 900.000 | 800.000 | -11.1% | -25.0% |" in table - - -def test_row_rejects_zero_peer_time_before_markdown_rendering() -> None: - with pytest.raises(ValueError, match="finite positive number"): - criterion_dim_plot.Row( - dim=2, - la_time=10.0, - la_lo=9.0, - la_hi=11.0, - na_time=0.0, - na_lo=0.0, - na_hi=0.0, - fa_time=100.0, - fa_lo=90.0, - fa_hi=110.0, - ) - - -def test_gp_quote_escapes_backslashes_and_quotes() -> None: - assert criterion_dim_plot._gp_quote("plain") == "'plain'" - assert criterion_dim_plot._gp_quote("a'b") == "'a\\'b'" - assert criterion_dim_plot._gp_quote("a\\b") == "'a\\\\b'" - assert criterion_dim_plot._gp_quote("a\\'b") == "'a\\\\\\'b'" - - -def test_update_readme_table_replaces_only_between_markers(tmp_path: Path) -> None: - marker_begin, marker_end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - - readme = tmp_path / "README.md" - readme.write_bytes(f"# Title\r\nbefore\r\n{marker_begin}\r\nold line 1\r\nold line 2\r\n{marker_end}\r\nafter\r\n".encode()) - - table_md = "| a |\n|---|\n| 1 |" - - changed = criterion_dim_plot._update_readme_table(readme, marker_begin, marker_end, table_md) - assert changed is True - - text = readme.read_text(encoding="utf-8") - assert readme.read_bytes().startswith(b"# Title\r\nbefore\r\n") - assert readme.read_bytes().endswith(b"\r\nafter\r\n") - assert "old line 1" not in text - assert "old line 2" not in text - assert marker_begin in text - assert marker_end in text - assert "| a |" in text - assert f"{marker_begin}\n\n| a |\n|---|\n| 1 |\n\n{marker_end}" in text - - # Re-running with the same content should be a no-op. - changed_again = criterion_dim_plot._update_readme_table(readme, marker_begin, marker_end, table_md) - assert changed_again is False - - -def _canonical_benchmark_readme(version: str) -> str: - begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - return ( - "[docs](https://github.com/acgetchell/la-stack/blob/v0.4.5/README.md)\n" - f"[csv](https://github.com/acgetchell/la-stack/blob/v{version}/docs/assets/bench/vs_linalg_lu_solve_median.csv)\n" - f"[provenance](https://github.com/acgetchell/la-stack/blob/v{version}/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json)\n" - f"[svg](https://raw.githubusercontent.com/acgetchell/la-stack/v{version}/docs/assets/bench/vs_linalg_lu_solve_median.svg)\n" - f"{begin}\nold table\n{end}\n" - ) - - -def test_replace_readme_benchmark_asset_versions_updates_only_complete_selected_asset_set() -> None: - updated = criterion_dim_plot._replace_readme_benchmark_asset_versions( - _canonical_benchmark_readme("1.2.2"), - metric="lu_solve", - stat="median", - version="1.2.3", - ) - - assert updated.count("v1.2.3/docs/assets/bench/") == 3 - assert "blob/v0.4.5/README.md" in updated - assert "old table" in updated - - -def test_replace_readme_benchmark_asset_versions_rejects_incomplete_asset_set() -> None: - readme = _canonical_benchmark_readme("1.2.2").replace( - "[provenance](https://github.com/acgetchell/la-stack/blob/v1.2.2/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json)\n", - "", - ) - - with pytest.raises(criterion_dim_plot.ReadmeBenchmarkLinkError, match="exactly one tag-pinned link"): - criterion_dim_plot._replace_readme_benchmark_asset_versions( - readme, - metric="lu_solve", - stat="median", - version="1.2.3", - ) - - -def _publication_args() -> criterion_dim_plot.PlotCliArgs: - return criterion_dim_plot.PlotCliArgs( - metric="lu_solve", - stat="median", - sample="new", - criterion_dir="target/criterion", - performance_csv=criterion_dim_plot._DEFAULT_PERFORMANCE_CSV, - out=None, - csv=None, - log_y=True, - no_plot=False, - update_readme=True, - readme="README.md", - allow_partial=False, - ) - - -def test_readme_publication_rejects_noncanonical_data_and_asset_paths_before_rendering( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - (tmp_path / "README.md").write_text(f"{begin}\nold\n{end}\n", encoding="utf-8") - - assert ( - criterion_dim_plot._validate_readme_target( - tmp_path, - replace(_publication_args(), performance_csv="stale-results.csv"), - ) - == 2 - ) - assert "canonical performance-release input" in capsys.readouterr().err - - assert ( - criterion_dim_plot._validate_readme_target( - tmp_path, - replace(_publication_args(), csv="custom.csv"), - ) - == 2 - ) - assert "canonical CSV/SVG destinations" in capsys.readouterr().err - - -def test_readme_publication_rejects_no_plot_before_rendering( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - (tmp_path / "README.md").write_text(f"{begin}\nold\n{end}\n", encoding="utf-8") - - assert ( - criterion_dim_plot._validate_readme_target( - tmp_path, - replace(_publication_args(), no_plot=True), - ) - == 2 - ) - assert "--no-plot is exploratory-only" in capsys.readouterr().err - - -def test_fixture_readme_may_use_custom_asset_destinations(tmp_path: Path) -> None: - begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - fixture = tmp_path / "README.fixture.md" - fixture.write_text(f"{begin}\nold\n{end}\n", encoding="utf-8") - - assert ( - criterion_dim_plot._validate_readme_target( - tmp_path, - replace( - _publication_args(), - readme=str(fixture), - csv="custom.csv", - out="custom.svg", - ), - ) - == 0 - ) - - -def test_readme_publication_rejects_incomplete_benchmark_links_before_rendering( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - (tmp_path / "Cargo.toml").write_text('[package]\nname = "fixture"\nversion = "1.2.3"\n', encoding="utf-8") - readme = _canonical_benchmark_readme("1.2.2").replace( - "[svg](https://raw.githubusercontent.com/acgetchell/la-stack/v1.2.2/docs/assets/bench/vs_linalg_lu_solve_median.svg)\n", - "", - ) - (tmp_path / "README.md").write_text(readme, encoding="utf-8") - - assert criterion_dim_plot._validate_readme_target(tmp_path, _publication_args()) == 2 - assert "exactly one tag-pinned link" in capsys.readouterr().err - - -_TEST_COMMIT = "0123456789abcdef0123456789abcdef01234567" - - -def _timing(value: float) -> TimingEstimate: - return TimingEstimate(median_ns=value, ci_lower_ns=value * 0.9, ci_upper_ns=value * 1.1) - - -def _write_benchmark_checkout(root: Path, *, version: str = "0.4.6") -> None: - (root / "Cargo.toml").write_text( - f'[package]\nname = "fixture"\nversion = "{version}"\n[dev-dependencies]\ncriterion = "0.7"\nnalgebra = "0.34"\nfaer = "0.22"\n', - encoding="utf-8", - ) - (root / "Cargo.lock").write_text("version = 4\n", encoding="utf-8") - (root / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "1.88.0"\n', encoding="utf-8") - (root / "justfile").write_text("test-bench-inputs:\n", encoding="utf-8") - for relative in ( - ".config/nextest.toml", - "tests/exact_bench_config.rs", - "benches/comparison/tests/vs_linalg_inputs.rs", - "benches/vs_linalg.rs", - "src/lib.rs", - ): - path = root / relative - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("// fixture\n", encoding="utf-8") - - -def _write_performance_bundle( - root: Path, - *, - version: str = "0.4.6", - omit_dim: int | None = None, - omit_peer_dim: int | None = None, - include_contract: bool = True, -) -> ArtifactPaths: - harness_sha256, missing = criterion_dim_plot._provenance_harness_digest(root) - source_sha256, source_missing = criterion_dim_plot._source_state_digest(root) - assert missing == [] - assert source_missing is False - cargo_lock_sha256 = hashlib.sha256((root / "Cargo.lock").read_bytes()).hexdigest() - baseline_commit = "fedcba9876543210fedcba9876543210fedcba98" - baseline_source_sha256 = "b" * 64 - environment: dict[str, object] = { - "cargo_lock_sha256": cargo_lock_sha256, - "commit": _TEST_COMMIT, - "correctness_gate": "passed", - "cpu": "test-cpu", - "git_clean": False, - "harness_sha256": harness_sha256, - "os": "TestOS 1 arm64", - "rustc": "rustc 1.88.0", - "source_state_sha256": source_sha256, - } - if include_contract: - environment["benchmark_contract_sha256"] = criterion_dim_plot.benchmark_contract_digest(root) - current_tag = f"v{version}" - baseline_tag = "v0.4.5" - bundle = PerformanceBundle( - context=ArtifactContext( - release=ReleasePair(current=current_tag, baseline=baseline_tag), - statistic="median", - suite="all", - scope="release-signal", - source=ReportSource( - version=version, - commit=_TEST_COMMIT, - ref="HEAD", - revision_timestamp="2026-08-20 12:00:00 UTC", - ), - benchmark_provenance={ - "baseline": baseline_tag, - "current": current_tag, - "criterion": { - "baseline_command": ["cargo", "bench", "--baseline", baseline_tag], - "criterion_version": "0.7.0", - "current_command": ["cargo", "bench", "--save-baseline", "current"], - "sample": "new", - "scope": "release-signal", - "statistic": "median", - "suite": "all", - }, - "measurement": { - "baseline_api_compatibility": "la_stack_pre_rational_input_api", - "baseline_commit": baseline_commit, - "baseline_git_clean": False, - "baseline_source_state_sha256": baseline_source_sha256, - "current_commit": _TEST_COMMIT, - "current_git_clean": False, - "current_source_state_sha256": source_sha256, - "status": "recorded", - **{key: environment[key] for key in ("cargo_lock_sha256", "cpu", "harness_sha256", "os", "rustc")}, - **({"benchmark_contract_sha256": environment["benchmark_contract_sha256"]} if "benchmark_contract_sha256" in environment else {}), - }, - "mode": "shared-current-harness", - "publication": environment, - "schema": 2, - "validation": { - "baseline_api_compatibility": "la_stack_pre_rational_input_api", - "baseline_commit": baseline_commit, - "baseline_git_clean": False, - "baseline_revision": "passed", - "baseline_source_state_sha256": baseline_source_sha256, - "command": ["just", "test-bench-inputs"], - "current_commit": _TEST_COMMIT, - "current_git_clean": False, - "current_revision": "passed", - "current_source_state_sha256": source_sha256, - "harness": "shared-current", - "shared_harness_rational_inputs": False, - }, - }, - ), - rows=tuple( - PerformanceRow( - suite="vs_linalg", - scope="release-signal", - benchmark_id=f"d{dim}/la_stack_lu_solve", - group=f"d{dim}", - benchmark="la_stack_lu_solve", - baseline_benchmark="la_stack_lu_solve", - coverage_status="comparable", - coverage_note="", - baseline=_timing(float(dim * 6)), - current=_timing(float(dim * 5)), - baseline_nalgebra=None if dim == omit_peer_dim else _timing(float(dim * 10)), - baseline_faer=_timing(float(dim * 20)), - ) - for dim in criterion_dim_plot.CANONICAL_DIMS - if dim != omit_dim - ), - ) - paths = ArtifactPaths( - csv=root / criterion_dim_plot._DEFAULT_PERFORMANCE_CSV, - provenance=root / "target/bench-reports/performance.provenance.json", - ) - write_bundle(paths, bundle) - return paths - - -def test_main_update_readme_happy_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - _write_benchmark_checkout(tmp_path) - performance_paths = _write_performance_bundle(tmp_path) - readme = tmp_path / "README.md" - readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") - - def fake_run_git(_command: str, args: list[str], **_kwargs: object) -> SimpleNamespace: - return SimpleNamespace(stdout=f"{_TEST_COMMIT}\n") - - monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - monkeypatch.setattr(criterion_dim_plot, "run_command", fake_run_git) - - def fake_render(request: criterion_dim_plot.PlotRequest) -> None: - request.out_svg.write_text("\n", encoding="utf-8") - - monkeypatch.setattr(criterion_dim_plot, "_render_svg_with_gnuplot", fake_render) - - out_csv = tmp_path / "docs" / "assets" / "bench" / "vs_linalg_lu_solve_median.csv" - out_svg = tmp_path / "docs" / "assets" / "bench" / "vs_linalg_lu_solve_median.svg" - - rc = criterion_dim_plot.main( - [ - "--metric", - "lu_solve", - "--stat", - "median", - "--sample", - "new", - "--update-readme", - ] - ) - assert rc == 0 - assert out_svg.read_text(encoding="utf-8") == "\n" - - # CSV written - csv_text = out_csv.read_text(encoding="utf-8") - assert csv_text.startswith("D,la_stack,la_lo,la_hi,nalgebra,na_lo,na_hi,faer,fa_lo,fa_hi\n") - assert "2,10.0" in csv_text - assert "64,320.0" in csv_text - - # README updated with computed table - readme_text = readme.read_text(encoding="utf-8") - assert "old table" not in readme_text - assert "| 2 | 10.000 | 20.000 | 40.000 | +50.0% | +75.0% |" in readme_text - assert "| 64 | 320.000 | 640.000 | 1,280.000 | +50.0% | +75.0% |" in readme_text - assert readme_text.count("v0.4.6/docs/assets/bench/") == 3 - assert "blob/v0.4.5/README.md" in readme_text - - provenance = json.loads(out_csv.with_suffix(".provenance.json").read_text(encoding="utf-8")) - assert provenance["measurement"]["status"] == "recorded" - assert provenance["measurement"]["source"] == "retained performance-release artifact" - assert provenance["measurement"]["la_stack_sample"] == "current" - assert provenance["measurement"]["peer_release_context"] == "v0.4.5" - assert provenance["measurement"]["peer_sample"] == "baseline phase under shared current harness" - assert provenance["publication"]["correctness_gate"] == "validated-performance-release-artifact" - assert provenance["publication"]["benchmark_contract"] == "matched" - assert "git_clean" not in provenance["publication"] - assert provenance["criterion"]["current_command"][:2] == ["cargo", "bench"] - assert provenance["performance_artifact"]["csv"] == "target/bench-reports/performance.csv" - assert provenance["performance_artifact"]["csv_sha256"] == hashlib.sha256(performance_paths.csv.read_bytes()).hexdigest() - assert re.fullmatch(r"[0-9a-f]{64}", provenance["publication"]["source_state_sha256"]) - - published = (readme, out_csv, out_svg, out_csv.with_suffix(".provenance.json")) - first = {path: (path.read_bytes(), path.stat().st_mtime_ns) for path in published} - assert criterion_dim_plot.main(["--metric", "lu_solve", "--stat", "median", "--sample", "new", "--update-readme"]) == 0 - assert {path: (path.read_bytes(), path.stat().st_mtime_ns) for path in published} == first - - -def test_dim_parsing_and_discovery(tmp_path: Path) -> None: - assert criterion_dim_plot._dim_from_group_dir("d2") == 2 - assert criterion_dim_plot._dim_from_group_dir("d10") == 10 - assert criterion_dim_plot._dim_from_group_dir("dx") is None - assert criterion_dim_plot._dim_from_group_dir("2") is None - - (tmp_path / "d2").mkdir() - (tmp_path / "d10").mkdir() - (tmp_path / "not_a_dim").mkdir() - dims = criterion_dim_plot._discover_dims(tmp_path) - assert dims == [2, 10] - - -def test_toml_helpers_read_versions(tmp_path: Path) -> None: - cargo_toml = tmp_path / "Cargo.toml" - cargo_toml.write_text( - ( - "# comment line\n" - "[package]\n" - 'version = "1.2.3" # inline comment\n' - "\n" - "[dependencies]\n" - 'nalgebra = "0.34.0"\n' - 'faer = { version = "0.21.4" }\n' - "\n" - "[dev-dependencies]\n" - 'serde = "1.0"' - ), - encoding="utf-8", - ) - - assert criterion_dim_plot._read_cargo_package_version(cargo_toml) == "1.2.3" - deps = criterion_dim_plot._read_cargo_dependency_versions(cargo_toml, {"nalgebra", "faer"}) - assert deps["nalgebra"] == "0.34.0" - assert deps["faer"] == "0.21.4" - - -def test_format_legend_label() -> None: - assert criterion_dim_plot._format_legend_label("la-stack", "0.4.6") == "la-stack v0.4.6" - assert criterion_dim_plot._format_legend_label("faer", "unknown") == "faer" - - -@pytest.mark.parametrize("interval", [None, {"lower_bound": 4.0}]) -def test_plot_adapter_requires_complete_confidence_interval(tmp_path: Path, interval: dict[str, float] | None) -> None: - estimates = tmp_path / "estimates.json" - estimates.write_text(json.dumps({"median": {"point_estimate": 5.0, "confidence_interval": interval}}), encoding="utf-8") - - with pytest.raises(ValueError, match=r"confidence interval|upper_bound"): - criterion_dim_plot._read_estimate(estimates, "median") - - -def test_row_rejects_invalid_dimension_and_times() -> None: - with pytest.raises(ValueError, match="dimension must be positive"): - criterion_dim_plot.Row( - dim=0, - la_time=1.0, - la_lo=1.0, - la_hi=1.0, - na_time=1.0, - na_lo=1.0, - na_hi=1.0, - fa_time=1.0, - fa_lo=1.0, - fa_hi=1.0, - ) - - with pytest.raises(ValueError, match="finite positive number"): - criterion_dim_plot.Row( - dim=2, - la_time=float("inf"), - la_lo=1.0, - la_hi=1.0, - na_time=1.0, - na_lo=1.0, - na_hi=1.0, - fa_time=1.0, - fa_lo=1.0, - fa_hi=1.0, - ) - - row = criterion_dim_plot.Row( - dim=2, - la_time=5.0, - la_lo=1.0, - la_hi=4.0, - na_time=1.0, - na_lo=1.0, - na_hi=1.0, - fa_time=1.0, - fa_lo=1.0, - fa_hi=1.0, - ) - assert row.la_time == 5.0 - - -def test_write_csv_and_collect_rows(tmp_path: Path) -> None: - rows = [ - criterion_dim_plot.Row( - dim=2, - la_time=1.0, - la_lo=0.9, - la_hi=1.1, - na_time=2.0, - na_lo=1.9, - na_hi=2.1, - fa_time=3.0, - fa_lo=2.9, - fa_hi=3.1, - ) - ] - out_csv = tmp_path / "out.csv" - criterion_dim_plot._write_csv(out_csv, rows) - text = out_csv.read_text(encoding="utf-8") - assert text.startswith("D,la_stack,la_lo,la_hi,nalgebra,na_lo,na_hi,faer,fa_lo,fa_hi") - assert "2,1.0,0.9,1.1,2.0,1.9,2.1,3.0,2.9,3.1" in text - - criterion_dir = tmp_path / "criterion" - metric = criterion_dim_plot.METRICS["lu_solve"] - d2 = criterion_dir / "d2" - d2.mkdir(parents=True) - # Only la_stack exists; should be skipped. - (d2 / metric.la_bench / "new").mkdir(parents=True) - (d2 / metric.la_bench / "new" / "estimates.json").write_text( - json.dumps({"median": {"point_estimate": 1.0}}), - encoding="utf-8", - ) - rows2, skipped = criterion_dim_plot._collect_rows(criterion_dir, [2], metric, "median", "new") - assert rows2 == [] - assert skipped == ["d2 (missing nalgebra_lu_solve, faer_lu_solve)"] - - -def test_resolve_paths(tmp_path: Path) -> None: - root = tmp_path - resolved = criterion_dim_plot._resolve_under_root(root, "foo/bar.csv") - assert resolved == root / "foo/bar.csv" - - svg, csv = criterion_dim_plot._resolve_output_paths(root, "lu_solve", "median", None, None) - assert svg == root / "docs/assets/bench/vs_linalg_lu_solve_median.svg" - assert csv == root / "docs/assets/bench/vs_linalg_lu_solve_median.csv" - - -def test_main_error_paths(tmp_path: Path) -> None: - # Missing Criterion directory. - rc = criterion_dim_plot.main( - [ - "--criterion-dir", - str(tmp_path / "missing"), - "--no-plot", - ] - ) - assert rc == 2 - - # Criterion directory exists but has no usable rows. - criterion_dir = tmp_path / "criterion" - (criterion_dir / "d2").mkdir(parents=True) - rc = criterion_dim_plot.main( - [ - "--criterion-dir", - str(criterion_dir), - "--metric", - "lu_solve", - "--stat", - "median", - "--sample", - "new", - "--no-plot", - ] - ) - assert rc == 2 - - -def test_main_requires_canonical_dimensions_before_writing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - criterion_dir = tmp_path / "criterion" - metric = criterion_dim_plot.METRICS["lu_solve"] - for bench in (metric.la_bench, metric.na_bench, metric.fa_bench): - estimates = criterion_dir / "d2" / bench / "new" / "estimates.json" - estimates.parent.mkdir(parents=True, exist_ok=True) - estimates.write_text( - json.dumps( - { - "median": { - "point_estimate": 1.0, - "confidence_interval": {"lower_bound": 0.9, "upper_bound": 1.1}, - } - } - ), - encoding="utf-8", - ) - output = tmp_path / "out.csv" - monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - - rc = criterion_dim_plot.main(["--criterion-dir", str(criterion_dir), "--csv", str(output), "--no-plot"]) - - assert rc == 2 - assert not output.exists() - assert not output.with_suffix(".provenance.json").exists() - - -def test_main_partial_mode_is_explicit_and_labels_measurement_unavailable( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - criterion_dir = tmp_path / "criterion" - metric = criterion_dim_plot.METRICS["lu_solve"] - for bench in (metric.la_bench, metric.na_bench, metric.fa_bench): - estimates = criterion_dir / "d2" / bench / "new" / "estimates.json" - estimates.parent.mkdir(parents=True, exist_ok=True) - estimates.write_text( - json.dumps( - { - "median": { - "point_estimate": 1.0, - "confidence_interval": {"lower_bound": 0.9, "upper_bound": 1.1}, - } - } - ), - encoding="utf-8", - ) - output = tmp_path / "out.csv" - monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - - rc = criterion_dim_plot.main( - [ - "--criterion-dir", - str(criterion_dir), - "--csv", - str(output), - "--no-plot", - "--allow-partial", - ] - ) - - assert rc == 0 - provenance = json.loads(output.with_suffix(".provenance.json").read_text(encoding="utf-8")) - assert provenance["measurement"]["status"] == "unavailable" - assert provenance["publication"]["correctness_gate"] == "not-run-exploratory" - - -def test_main_rejects_missing_confidence_interval_without_writing( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - criterion_dir = tmp_path / "criterion" - metric = criterion_dim_plot.METRICS["lu_solve"] - for bench in (metric.la_bench, metric.na_bench, metric.fa_bench): - estimates = criterion_dir / "d2" / bench / "new" / "estimates.json" - estimates.parent.mkdir(parents=True, exist_ok=True) - estimates.write_text(json.dumps({"median": {"point_estimate": 1.0}}), encoding="utf-8") - output = tmp_path / "out.csv" - monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - - rc = criterion_dim_plot.main( - [ - "--criterion-dir", - str(criterion_dir), - "--csv", - str(output), - "--no-plot", - "--allow-partial", - ] - ) - - assert rc == 2 - assert "Invalid Criterion estimate data" in capsys.readouterr().err - assert not output.exists() - - -def test_main_rejects_overflowing_timing_without_writing_or_traceback( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - criterion_dir = tmp_path / "criterion" - metric = criterion_dim_plot.METRICS["lu_solve"] - for bench in (metric.la_bench, metric.na_bench, metric.fa_bench): - estimates = criterion_dir / "d2" / bench / "new" / "estimates.json" - estimates.parent.mkdir(parents=True, exist_ok=True) - estimates.write_text(json.dumps({"median": {"point_estimate": _OVERFLOWING_TIMING}}), encoding="utf-8") - output = tmp_path / "out.csv" - monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - - rc = criterion_dim_plot.main( - [ - "--criterion-dir", - str(criterion_dir), - "--csv", - str(output), - "--no-plot", - "--allow-partial", - ] - ) - - assert rc == 2 - assert "Invalid Criterion estimate data" in capsys.readouterr().err - assert not output.exists() - - -@pytest.mark.parametrize( - "failure", - [ - subprocess.TimeoutExpired(["tool"], 17), - OSError("working directory unavailable"), - ], -) -def test_provenance_helpers_treat_timeout_and_os_error_as_unavailable( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - failure: Exception, -) -> None: - def fail_command(*_args: object, **_kwargs: object) -> SimpleNamespace: - raise failure - - monkeypatch.setattr(criterion_dim_plot, "run_command", fail_command) - assert criterion_dim_plot._git_value(tmp_path, ["rev-parse", "HEAD"]) == "unavailable" - git_clean, _status_digest = criterion_dim_plot._git_status_metadata(tmp_path) - assert git_clean is None - - monkeypatch.setattr(criterion_dim_plot, "run_command", fail_command) - assert criterion_dim_plot._rustc_version(tmp_path) == "unavailable" - - -def _mock_publication_environment(root: Path, monkeypatch: pytest.MonkeyPatch) -> None: - def fake_run_git(_command: str, args: list[str], **_kwargs: object) -> SimpleNamespace: - return SimpleNamespace(stdout=f"{_TEST_COMMIT}\n") - - monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: root) - monkeypatch.setattr(criterion_dim_plot, "run_command", fake_run_git) - - -def test_main_publication_fails_closed_when_git_commit_is_unavailable( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - _write_benchmark_checkout(tmp_path) - _write_performance_bundle(tmp_path) - readme = tmp_path / "README.md" - readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") - monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - - def unavailable_git(*_args: object, **_kwargs: object) -> SimpleNamespace: - message = "git unavailable" - raise OSError(message) - - monkeypatch.setattr(criterion_dim_plot, "run_command", unavailable_git) - - assert criterion_dim_plot.main(["--update-readme"]) == 2 - assert "current checkout commit 'unavailable'" in capsys.readouterr().err - assert "old table" in readme.read_text(encoding="utf-8") - assert not (tmp_path / "docs/assets/bench/vs_linalg_lu_solve_median.csv").exists() - - -def test_main_publication_rejects_missing_performance_bundle( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - _write_benchmark_checkout(tmp_path) - readme = tmp_path / "README.md" - readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") - _mock_publication_environment(tmp_path, monkeypatch) - - assert criterion_dim_plot.main(["--update-readme"]) == 2 - assert "artifact pair is incomplete" in capsys.readouterr().err - assert "old table" in readme.read_text(encoding="utf-8") - - -@pytest.mark.parametrize( - "case", - [ - (3, None, "missing d3/la_stack_lu_solve"), - (None, 5, "missing nalgebra peer timing"), - ], -) -def test_main_publication_rejects_incomplete_retained_coverage( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], - case: tuple[int | None, int | None, str], -) -> None: - omit_dim, omit_peer_dim, expected = case - _write_benchmark_checkout(tmp_path) - _write_performance_bundle(tmp_path, omit_dim=omit_dim, omit_peer_dim=omit_peer_dim) - readme = tmp_path / "README.md" - readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") - _mock_publication_environment(tmp_path, monkeypatch) - - assert criterion_dim_plot.main(["--update-readme"]) == 2 - assert expected in capsys.readouterr().err - assert "old table" in readme.read_text(encoding="utf-8") - - -def test_main_publication_rejects_stale_harness_without_writing( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - _write_benchmark_checkout(tmp_path) - _write_performance_bundle(tmp_path) - (tmp_path / "benches/vs_linalg.rs").write_text("// changed after measurement\n", encoding="utf-8") - readme = tmp_path / "README.md" - readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") - _mock_publication_environment(tmp_path, monkeypatch) - - assert criterion_dim_plot.main(["--update-readme"]) == 2 - assert "benchmark_contract_sha256" in capsys.readouterr().err - assert "old table" in readme.read_text(encoding="utf-8") - - -def test_main_publication_rejects_benchmark_recipe_changes( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - _write_benchmark_checkout(tmp_path) - _write_performance_bundle(tmp_path) - (tmp_path / "justfile").write_text("test-bench-inputs:\n cargo test --test changed-input-gate\n", encoding="utf-8") - readme = tmp_path / "README.md" - readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") - _mock_publication_environment(tmp_path, monkeypatch) - - assert criterion_dim_plot.main(["--update-readme"]) == 2 - assert "benchmark_contract_sha256" in capsys.readouterr().err - assert "old table" in readme.read_text(encoding="utf-8") - - -def test_main_publication_labels_legacy_artifact_without_contract( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - _write_benchmark_checkout(tmp_path) - _write_performance_bundle(tmp_path, include_contract=False) - readme = tmp_path / "README.md" - readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") - _mock_publication_environment(tmp_path, monkeypatch) - monkeypatch.setattr( - criterion_dim_plot, - "_render_svg_with_gnuplot", - lambda request: request.out_svg.write_text("\n", encoding="utf-8"), - ) - - assert criterion_dim_plot.main(["--update-readme"]) == 0 - provenance = json.loads((tmp_path / "docs/assets/bench/vs_linalg_lu_solve_median.provenance.json").read_text(encoding="utf-8")) - assert provenance["publication"]["benchmark_contract"] == "legacy-retained-artifact" - - -@pytest.mark.parametrize("changed", ["source", "lockfile", "release", "legacy-commit"]) -def test_main_publication_rejects_stale_measurement_identity( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - changed: str, -) -> None: - _write_benchmark_checkout(tmp_path) - _write_performance_bundle(tmp_path, include_contract=changed != "legacy-commit") - readme = tmp_path / "README.md" - original = _canonical_benchmark_readme("0.0.8") - readme.write_text(original, encoding="utf-8") - _mock_publication_environment(tmp_path, monkeypatch) - if changed == "source": - (tmp_path / "src/lib.rs").write_text("// changed source\n", encoding="utf-8") - elif changed == "lockfile": - with (tmp_path / "Cargo.lock").open("a", encoding="utf-8") as lock: - lock.write("\n# changed lock\n") - elif changed == "release": - manifest = tmp_path / "Cargo.toml" - manifest.write_text(manifest.read_text(encoding="utf-8").replace("0.4.6", "0.4.7"), encoding="utf-8") - else: - monkeypatch.setattr(criterion_dim_plot, "run_command", lambda *_args, **_kwargs: SimpleNamespace(stdout="b" * 40)) - - assert criterion_dim_plot.main(["--update-readme"]) == 2 - assert readme.read_text(encoding="utf-8") == original - assert not (tmp_path / "docs/assets/bench/vs_linalg_lu_solve_median.csv").exists() - - -def test_main_publication_rejects_tampered_performance_csv( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - _write_benchmark_checkout(tmp_path) - paths = _write_performance_bundle(tmp_path) - paths.csv.write_text(paths.csv.read_text(encoding="utf-8") + "tampered\n", encoding="utf-8") - readme = tmp_path / "README.md" - readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") - _mock_publication_environment(tmp_path, monkeypatch) - - assert criterion_dim_plot.main(["--update-readme"]) == 2 - assert "CSV digest mismatch" in capsys.readouterr().err - assert "old table" in readme.read_text(encoding="utf-8") - - -def test_staged_publication_leaves_existing_assets_unchanged_when_render_fails( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - csv_path = tmp_path / "benchmark.csv" - svg_path = tmp_path / "benchmark.svg" - provenance_path = csv_path.with_suffix(".provenance.json") - readme = tmp_path / "README.md" - for path, text in ( - (csv_path, "old csv\n"), - (svg_path, "old svg\n"), - (provenance_path, "old provenance\n"), - ): - path.write_text(text, encoding="utf-8") - begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - readme.write_text(f"before\n{begin}\nold table\n{end}\nafter\n", encoding="utf-8") - - def fail_render(_request: criterion_dim_plot.PlotRequest) -> None: - raise subprocess.CalledProcessError(1, ["gnuplot"], stderr="render failed") - - monkeypatch.setattr(criterion_dim_plot, "_render_svg_with_gnuplot", fail_render) - args = criterion_dim_plot.PlotCliArgs( - metric="lu_solve", - stat="median", - sample="new", - criterion_dir="target/criterion", - performance_csv=criterion_dim_plot._DEFAULT_PERFORMANCE_CSV, - out=str(svg_path), - csv=str(csv_path), - log_y=False, - no_plot=False, - update_readme=True, - readme=str(readme), - allow_partial=False, - ) - row = criterion_dim_plot.Row(2, 1.0, 0.9, 1.1, 2.0, 1.9, 2.1, 3.0, 2.9, 3.1) - request = criterion_dim_plot.PlotRequest( - csv_path=csv_path, - out_svg=svg_path, - title="title", - stat="median", - dims=(2,), - la_label="la-stack", - na_label="nalgebra", - fa_label="faer", - log_y=False, - ) - - rc = criterion_dim_plot._stage_and_publish_outputs( - root=tmp_path, - args=args, - rows=[row], - req=request, - provenance={"schema": 1}, - skipped=[], - ) - - assert rc == 1 - assert csv_path.read_text(encoding="utf-8") == "old csv\n" - assert svg_path.read_text(encoding="utf-8") == "old svg\n" - assert provenance_path.read_text(encoding="utf-8") == "old provenance\n" - assert "old table" in readme.read_text(encoding="utf-8") - - -def test_main_restores_complete_publication_when_readme_replace_fails( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - _write_benchmark_checkout(tmp_path) - _write_performance_bundle(tmp_path) - _mock_publication_environment(tmp_path, monkeypatch) - readme = tmp_path / "README.md" - before = {readme: _canonical_benchmark_readme("0.0.8").replace("\n", "\r\n").encode()} - for suffix in ("csv", "svg", "provenance.json"): - before[tmp_path / f"docs/assets/bench/vs_linalg_lu_solve_median.{suffix}"] = f"previous {suffix}\n".encode() - for path, payload in before.items(): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(payload) - - def render(request: criterion_dim_plot.PlotRequest) -> None: - request.out_svg.write_bytes(b"\n") - - real_replace = Path.replace - - def fail_readme(source: Path, destination: Path) -> Path: - if destination == readme: - msg = "README publication failed" - raise OSError(msg) - return real_replace(source, destination) - - monkeypatch.setattr(criterion_dim_plot, "_render_svg_with_gnuplot", render) - monkeypatch.setattr(Path, "replace", fail_readme) - - assert criterion_dim_plot.main(["--update-readme"]) == 2 - captured = capsys.readouterr() - assert "README publication failed" in captured.err - assert "Wrote" not in captured.out - assert {path: path.read_bytes() for path in before} == before - - -def test_shared_publication_keeps_existing_plot_when_provenance_target_is_invalid(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - csv_path, provenance = tmp_path / "plot.csv", tmp_path / "plot.provenance.json" - staged_csv, staged_provenance = tmp_path / "candidate.csv", tmp_path / "candidate.json" - csv_path.write_bytes(b"old CSV") - provenance.mkdir() - staged_csv.write_bytes(b"new CSV") - staged_provenance.write_bytes(b"new provenance") - - assert not criterion_dim_plot._publish_staged_files([(staged_csv, csv_path), (staged_provenance, provenance)]) - assert csv_path.read_bytes() == b"old CSV" - assert provenance.is_dir() - assert "could not publish benchmark artifacts" in capsys.readouterr().err - - -def test_repo_root_resolution_uses_working_checkout_for_installed_entrypoint(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - nested = tmp_path / "nested" - nested.mkdir() - (tmp_path / "Cargo.toml").write_text("[package]\n", encoding="utf-8") - monkeypatch.chdir(nested) - - assert criterion_dim_plot._repo_root() == tmp_path - - -def test_publication_paths_reject_output_aliases(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - output = tmp_path / "benchmark.csv" - args = criterion_dim_plot.PlotCliArgs( - metric="lu_solve", - stat="median", - sample="new", - criterion_dir="target/criterion", - performance_csv=criterion_dim_plot._DEFAULT_PERFORMANCE_CSV, - out=str(output), - csv=str(output), - log_y=False, - no_plot=False, - update_readme=False, - readme="README.md", - allow_partial=False, - ) - - rc = criterion_dim_plot._validate_publication_paths(tmp_path, args, out_svg=output, out_csv=output) - - assert rc == 2 - assert "must use distinct paths" in capsys.readouterr().err diff --git a/scripts/tests/test_maintenance_integration.py b/scripts/tests/test_maintenance_integration.py index 2f6e261..c791424 100644 --- a/scripts/tests/test_maintenance_integration.py +++ b/scripts/tests/test_maintenance_integration.py @@ -128,3 +128,14 @@ def test_semgrep_adapter_uses_real_consumer_rules_and_fixtures() -> None: assert config["namespace"] == "la-stack." result = dry_run(REPO_ROOT, "semgrep-test") assert "research-repo-tools semgrep check-fixtures" in result.stderr + + +def test_semgrep_keeps_its_current_version_outside_the_python_315_environment() -> None: + """The generic dev updater cannot downgrade the isolated native scanner.""" + root = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + scanner = tomllib.loads((REPO_ROOT / "tooling/semgrep/pyproject.toml").read_text(encoding="utf-8")) + assert not any(isinstance(item, str) and item.startswith("semgrep") for item in root["dependency-groups"]["dev"]) + assert scanner["project"]["dependencies"] == ["semgrep==1.180.0"] + assert scanner["project"]["requires-python"] == ">=3.14,<3.15" + result = run_command("uv", ["run", "--locked", "--project", "tooling/semgrep", "semgrep", "--version"], cwd=REPO_ROOT) + assert result.stdout.strip() == "1.180.0" diff --git a/scripts/tests/test_performance_artifacts.py b/scripts/tests/test_performance_artifacts.py deleted file mode 100644 index f3f8683..0000000 --- a/scripts/tests/test_performance_artifacts.py +++ /dev/null @@ -1,566 +0,0 @@ -"""Tests for durable release-performance report artifacts.""" - -import csv -import hashlib -import io -import json -from collections.abc import Mapping -from types import MappingProxyType -from typing import TYPE_CHECKING, cast - -import pytest - -import performance_artifacts - -if TYPE_CHECKING: - from pathlib import Path -from performance_artifacts import ( - ArtifactContext, - ArtifactPaths, - PerformanceBundle, - PerformanceRow, - ReleasePair, - ReportSource, - TimingEstimate, - bundle_outputs, - load_bundle, - load_bundle_bytes, - serialize_bundle, - write_bundle, -) - - -def _timing(value: float) -> TimingEstimate: - return TimingEstimate( - median_ns=value, - ci_lower_ns=value * 0.9, - ci_upper_ns=value * 1.1, - ) - - -@pytest.mark.parametrize( - "case", - [ - ( - "v0.4.5", - "v0.4.4", - False, - "pre-rational-input", - "pre-rational-input", - "la_stack_pre_rational_input_api", - "excluded", - ), - ( - "v0.4.5", - "v0.4.5", - True, - "pre-rational-input", - "pre-rational-input", - "la_stack_pre_rational_input_api", - "current-only", - ), - ( - "v0.4.6-rc.1", - "v0.4.4", - True, - "rational-input", - "pre-rational-input", - "la_stack_pre_rational_input_api", - "current-only", - ), - ( - "v1.0.0-alpha.1", - "v0.4.5-beta.2", - True, - "rational-input", - "pre-rational-input", - "la_stack_pre_rational_input_api", - "current-only", - ), - ( - "v0.4.6", - "v0.4.6-rc.1", - True, - "rational-input", - "rational-input", - "none", - "comparable", - ), - ], -) -def test_shared_harness_compatibility_uses_literal_release_capability_oracle( - case: tuple[str, str, bool, str, str, str, str], -) -> None: - current, baseline, harness_rational, expected_current, expected_baseline, adapter, coverage = case - resolved = performance_artifacts.resolve_shared_harness_compatibility( - current=current, - baseline=baseline, - shared_harness_rational_inputs=harness_rational, - ) - - assert resolved.current == expected_current - assert resolved.baseline == expected_baseline - assert resolved.baseline_api_compatibility == adapter - assert resolved.rational_input_coverage == coverage - - -@pytest.mark.parametrize("release", ["v0.4.3", "0.4.3", "v0.4.3-rc.1", "v0.4.2", "v0.3.9"]) -@pytest.mark.parametrize("side", ["current", "baseline"]) -def test_shared_harness_rejects_unsupported_releases(release: str, side: str) -> None: - pair = {"current": "v0.4.6", "baseline": "v0.4.5", side: release} - with pytest.raises(ValueError, match=r"comparisons require v0\.4\.4 or newer"): - performance_artifacts.resolve_shared_harness_compatibility(**pair, shared_harness_rational_inputs=True) - - -def _context( - *, - current: str = "v0.4.6", - baseline: str = "v0.4.5", - shared_harness_rational_inputs: bool = True, -) -> ArtifactContext: - compatibility = "la_stack_pre_rational_input_api" if baseline in {"v0.4.4", "v0.4.5"} else "none" - return ArtifactContext( - release=ReleasePair(current=current, baseline=baseline), - statistic="median", - suite="exact", - scope="release-signal", - source=ReportSource( - version=current.removeprefix("v"), - commit="abc1234", - ref="HEAD", - revision_timestamp="2026-08-04 12:00:00 UTC", - ), - benchmark_provenance={ - "baseline": baseline, - "current": current, - "criterion": { - "baseline_command": ["just", "bench-save-baseline", baseline], - "criterion_version": "0.7.0", - "current_command": ["just", "bench-latest"], - "sample": "new", - "scope": "release-signal", - "statistic": "median", - "suite": "exact", - }, - "measurement": { - "baseline_api_compatibility": compatibility, - "baseline_commit": "def5678", - "baseline_git_clean": True, - "baseline_source_state_sha256": "d" * 64, - "cargo_lock_sha256": "b" * 64, - "cpu": "test-cpu", - "current_commit": "abc1234", - "current_git_clean": True, - "current_source_state_sha256": "c" * 64, - "harness_sha256": "a" * 64, - "os": "TestOS 1 x86_64", - "rustc": "rustc 1.97.1", - "status": "recorded", - }, - "mode": "shared-current-harness", - "publication": { - "cargo_lock_sha256": "b" * 64, - "commit": "abc1234", - "correctness_gate": "passed", - "cpu": "test-cpu", - "git_clean": True, - "harness_sha256": "a" * 64, - "os": "TestOS 1 x86_64", - "rustc": "rustc 1.97.1", - "source_state_sha256": "c" * 64, - }, - "schema": 2, - "validation": { - "baseline_api_compatibility": compatibility, - "baseline_commit": "def5678", - "baseline_git_clean": True, - "baseline_revision": "passed", - "baseline_source_state_sha256": "d" * 64, - "command": ["just", "test-bench-inputs"], - "current_commit": "abc1234", - "current_git_clean": True, - "current_revision": "passed", - "current_source_state_sha256": "c" * 64, - "harness": "shared-current", - "shared_harness_rational_inputs": shared_harness_rational_inputs, - }, - }, - ) - - -def _bundle(*, current_value: float = 8.0) -> PerformanceBundle: - rows = ( - PerformanceRow( - suite="exact", - scope="release-signal", - benchmark_id="exact_d2/det_exact", - group="exact_d2", - benchmark="det_exact", - baseline_benchmark="det_exact", - coverage_status="comparable", - coverage_note="", - baseline=_timing(10.0), - current=_timing(current_value), - ), - PerformanceRow( - suite="exact", - scope="release-signal", - benchmark_id="exact_d3/current_only", - group="exact_d3", - benchmark="current_only", - baseline_benchmark="current_only", - coverage_status="current-only", - coverage_note="The baseline does not expose this correctness-compatible row.", - baseline=None, - current=_timing(12.0), - ), - PerformanceRow( - suite="exact", - scope="release-signal", - benchmark_id="exact_d4/baseline_only", - group="exact_d4", - benchmark="baseline_only", - baseline_benchmark="baseline_only", - coverage_status="baseline-only", - coverage_note="The current run does not expose this row.", - baseline=_timing(15.0), - current=None, - ), - ) - return PerformanceBundle(context=_context(), rows=rows) - - -def _replace_csv_and_digest(csv_payload: bytes, provenance_payload: bytes) -> tuple[bytes, bytes]: - provenance = json.loads(provenance_payload) - provenance["csv"]["sha256"] = hashlib.sha256(csv_payload).hexdigest() - return csv_payload, (json.dumps(provenance, indent=2, sort_keys=True) + "\n").encode() - - -def test_artifact_round_trip_preserves_comparable_and_one_sided_rows() -> None: - bundle = _bundle() - - csv_payload, provenance_payload = serialize_bundle(bundle) - parsed = load_bundle_bytes(csv_payload, provenance_payload, source="round-trip fixture") - - assert parsed == PerformanceBundle(context=bundle.context, rows=bundle.sorted_rows) - assert [row.coverage_status for row in parsed.rows] == ["comparable", "current-only", "baseline-only"] - assert csv_payload.endswith(b"\n") - assert provenance_payload.endswith(b"\n") - - -def test_bundle_rejects_rational_rows_when_historical_harness_did_not_emit_them() -> None: - rational_row = PerformanceRow( - suite="exact", - scope="release-signal", - benchmark_id="rational_input_d2/det_row_cleared_bareiss", - group="rational_input_d2", - benchmark="det_row_cleared_bareiss", - baseline_benchmark="det_row_cleared_bareiss", - coverage_status="current-only", - coverage_note="Baseline predates rational inputs.", - baseline=None, - current=_timing(12.0), - ) - context = _context( - current="v0.4.5", - baseline="v0.4.4", - shared_harness_rational_inputs=False, - ) - - with pytest.raises(ValueError, match="must be excluded"): - PerformanceBundle(context=context, rows=(rational_row,)) - - -def test_artifact_round_trip_allows_same_version_local_comparison() -> None: - original = _bundle() - bundle = PerformanceBundle( - context=_context(current="v0.4.4", baseline="v0.4.4"), - rows=original.rows, - ) - - csv_payload, provenance_payload = serialize_bundle(bundle) - parsed = load_bundle_bytes(csv_payload, provenance_payload, source="same-version local fixture") - - assert parsed.context.release == ReleasePair(current="v0.4.4", baseline="v0.4.4") - assert parsed.rows == bundle.sorted_rows - - -def test_artifact_serialization_is_independent_of_input_row_order() -> None: - bundle = _bundle() - reordered = PerformanceBundle(context=bundle.context, rows=tuple(reversed(bundle.rows))) - - assert serialize_bundle(reordered) == serialize_bundle(bundle) - - -def test_artifact_loader_rejects_csv_digest_mismatch() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - malformed = csv_payload.replace(b"exact_d2/det_exact", b"exact_d2/det_changed", 1) - - with pytest.raises(ValueError, match="CSV digest mismatch"): - load_bundle_bytes(malformed, provenance_payload, source="digest mismatch fixture") - - -def test_artifact_loader_rejects_csv_row_count_mismatch() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - provenance["csv"]["row_count"] += 1 - malformed = (json.dumps(provenance, indent=2, sort_keys=True) + "\n").encode() - - with pytest.raises(ValueError, match="CSV row count mismatch"): - load_bundle_bytes(csv_payload, malformed, source="row count mismatch fixture") - - -def test_artifact_loader_rejects_incomplete_confidence_interval() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - reader = csv.DictReader(io.StringIO(csv_payload.decode(), newline="")) - rows = list(reader) - rows[0]["current_ci_upper_ns"] = "" - output = io.StringIO(newline="") - fieldnames = reader.fieldnames - assert fieldnames is not None - writer = csv.DictWriter(output, fieldnames=fieldnames, lineterminator="\n") - writer.writeheader() - writer.writerows(rows) - malformed = output.getvalue().encode() - malformed, provenance_payload = _replace_csv_and_digest(malformed, provenance_payload) - - with pytest.raises(ValueError, match="incomplete current confidence interval"): - load_bundle_bytes(malformed, provenance_payload, source="incomplete interval fixture") - - -def test_artifact_loader_rejects_mismatched_release_pair() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - provenance["release"]["baseline"] = "v0.4.2" - malformed = (json.dumps(provenance, indent=2, sort_keys=True) + "\n").encode() - - with pytest.raises(ValueError, match="benchmark provenance baseline"): - load_bundle_bytes(csv_payload, malformed, source="mismatched release fixture") - - -def test_artifact_loader_rejects_malformed_csv_schema() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - malformed = csv_payload.replace(b"benchmark_id", b"benchmark_key", 1) - malformed, provenance_payload = _replace_csv_and_digest(malformed, provenance_payload) - - with pytest.raises(ValueError, match="unsupported release-performance CSV columns"): - load_bundle_bytes(malformed, provenance_payload, source="malformed CSV fixture") - - -def test_artifact_loader_rejects_malformed_provenance_json() -> None: - csv_payload, _provenance_payload = serialize_bundle(_bundle()) - - with pytest.raises(ValueError, match="malformed release-performance provenance JSON"): - load_bundle_bytes(csv_payload, b"{not-json}\n", source="malformed provenance fixture") - - -@pytest.mark.parametrize("invalid_schema", [True, 1.0]) -def test_artifact_loader_rejects_non_integer_outer_schema(invalid_schema: object) -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - provenance["schema_version"] = invalid_schema - - with pytest.raises(ValueError, match="unsupported release-performance provenance schema"): - load_bundle_bytes( - csv_payload, - (json.dumps(provenance) + "\n").encode(), - source="invalid outer schema fixture", - ) - - -@pytest.mark.parametrize("invalid_schema", [True, 2.0]) -def test_artifact_loader_rejects_non_integer_benchmark_schema(invalid_schema: object) -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - provenance["benchmark_provenance"]["schema"] = invalid_schema - - with pytest.raises(ValueError, match="benchmark provenance schema must be 2"): - load_bundle_bytes( - csv_payload, - (json.dumps(provenance) + "\n").encode(), - source="invalid benchmark schema fixture", - ) - - -def test_artifact_loader_rejects_incomplete_nested_provenance() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - del provenance["benchmark_provenance"]["criterion"]["criterion_version"] - - with pytest.raises(ValueError, match=r"criterion\.criterion_version"): - load_bundle_bytes( - csv_payload, - (json.dumps(provenance) + "\n").encode(), - source="incomplete nested provenance fixture", - ) - - -def test_artifact_loader_defaults_fields_absent_from_legacy_schema2_artifacts() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - benchmark_provenance = provenance["benchmark_provenance"] - assert benchmark_provenance["schema"] == 2 - del benchmark_provenance["current"] - del benchmark_provenance["validation"]["shared_harness_rational_inputs"] - - loaded = load_bundle_bytes( - csv_payload, - (json.dumps(provenance) + "\n").encode(), - source="legacy schema-2 artifact fixture", - ) - - loaded_provenance = loaded.context.benchmark_provenance - assert loaded_provenance["current"] == loaded.context.release.current - validation = loaded_provenance["validation"] - assert isinstance(validation, Mapping) - assert validation["shared_harness_rational_inputs"] is False - - -@pytest.mark.parametrize( - ("field", "value", "message"), - [ - ("current", False, r"root\.current"), - ("shared_harness_rational_inputs", "false", "must be a boolean"), - ], -) -def test_artifact_loader_rejects_invalid_present_legacy_default_fields( - field: str, - value: object, - message: str, -) -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - benchmark_provenance = provenance["benchmark_provenance"] - if field == "current": - benchmark_provenance[field] = value - else: - benchmark_provenance["validation"][field] = value - - with pytest.raises((TypeError, ValueError), match=message): - load_bundle_bytes( - csv_payload, - (json.dumps(provenance) + "\n").encode(), - source="invalid legacy-default field fixture", - ) - - -def test_artifact_loader_rejects_contradictory_current_revision() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - provenance["benchmark_provenance"]["validation"]["current_commit"] = "different-commit" - - with pytest.raises(ValueError, match=r"validation\.current_commit.*publication\.commit"): - load_bundle_bytes( - csv_payload, - (json.dumps(provenance) + "\n").encode(), - source="contradictory current revision fixture", - ) - - -def test_artifact_loader_rejects_recorded_measurement_without_cpu_model() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - provenance["benchmark_provenance"]["measurement"]["cpu"] = "unavailable" - provenance["benchmark_provenance"]["publication"]["cpu"] = "unavailable" - - with pytest.raises(ValueError, match="requires an identified CPU model"): - load_bundle_bytes( - csv_payload, - (json.dumps(provenance) + "\n").encode(), - source="unidentified CPU fixture", - ) - - -def test_bundle_rejects_duplicate_benchmark_keys() -> None: - bundle = _bundle() - - with pytest.raises(ValueError, match="duplicate benchmark key"): - PerformanceBundle(context=bundle.context, rows=(bundle.rows[0], bundle.rows[0])) - - -def test_artifact_context_freezes_nested_provenance() -> None: - context = _context() - - with pytest.raises(TypeError): - cast("dict[str, object]", context.benchmark_provenance)["schema"] = 3 - - criterion = context.benchmark_provenance["criterion"] - assert isinstance(criterion, MappingProxyType) - - -def test_artifact_loader_rejects_mode_measurement_contradiction() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - provenance["benchmark_provenance"]["mode"] = "historical-assets" - - with pytest.raises(ValueError, match="requires shared-current-harness mode"): - load_bundle_bytes(csv_payload, (json.dumps(provenance) + "\n").encode(), source="contradictory mode fixture") - - -def test_artifact_loader_rejects_arbitrary_compatibility_adapter() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - provenance["benchmark_provenance"]["validation"]["baseline_api_compatibility"] = "custom-adapter" - - with pytest.raises(ValueError, match="baseline_api_compatibility must be"): - load_bundle_bytes(csv_payload, (json.dumps(provenance) + "\n").encode(), source="invalid compatibility fixture") - - -def test_artifact_loader_accepts_pre_rational_adapter_for_v045() -> None: - bundle = _bundle() - context = _context(current="v0.4.6", baseline="v0.4.5") - csv_payload, provenance_payload = serialize_bundle(PerformanceBundle(context=context, rows=bundle.rows)) - - loaded = load_bundle_bytes( - csv_payload, - provenance_payload, - source="pre-rational compatibility fixture", - ) - - validation = loaded.context.benchmark_provenance["validation"] - assert isinstance(validation, Mapping) - assert validation["baseline_api_compatibility"] == "la_stack_pre_rational_input_api" - - -def test_artifact_loader_requires_recorded_measurement_compatibility() -> None: - csv_payload, provenance_payload = serialize_bundle(_bundle()) - provenance = json.loads(provenance_payload) - del provenance["benchmark_provenance"]["measurement"]["baseline_api_compatibility"] - - with pytest.raises(ValueError, match=r"measurement\.baseline_api_compatibility"): - load_bundle_bytes(csv_payload, (json.dumps(provenance) + "\n").encode(), source="missing compatibility fixture") - - -def test_artifact_loader_fails_closed_on_partial_pair(tmp_path: Path) -> None: - paths = ArtifactPaths( - csv=tmp_path / "performance.csv", - provenance=tmp_path / "performance.provenance.json", - ) - paths.csv.write_text("partial\n", encoding="utf-8") - - with pytest.raises(FileNotFoundError, match="artifact pair is incomplete"): - load_bundle(paths) - - -def test_bundle_candidates_round_trip_without_publishing(tmp_path: Path) -> None: - paths = ArtifactPaths(csv=tmp_path / "performance.csv", provenance=tmp_path / "performance.provenance.json") - bundle = _bundle() - outputs = bundle_outputs(paths, bundle) - - assert not paths.csv.exists() - assert not paths.provenance.exists() - assert load_bundle_bytes(outputs[paths.csv], outputs[paths.provenance], source="candidates") == PerformanceBundle( - context=bundle.context, rows=bundle.sorted_rows - ) - - -def test_shared_publication_rejects_invalid_pair_before_replacing_csv(tmp_path: Path) -> None: - paths = ArtifactPaths(csv=tmp_path / "performance.csv", provenance=tmp_path / "performance.provenance.json") - paths.csv.write_bytes(b"original CSV") - paths.provenance.mkdir() - - with pytest.raises(IsADirectoryError): - write_bundle(paths, _bundle()) - - assert paths.csv.read_bytes() == b"original CSV" - assert paths.provenance.is_dir() diff --git a/scripts/tests/test_performance_workflow.py b/scripts/tests/test_performance_workflow.py index a20d6ae..f8f575c 100644 --- a/scripts/tests/test_performance_workflow.py +++ b/scripts/tests/test_performance_workflow.py @@ -6,6 +6,7 @@ import os import shlex import shutil +import subprocess import sys import tarfile from dataclasses import replace @@ -13,21 +14,23 @@ from typing import TYPE_CHECKING import pytest +from research_repo_tools import publication from research_repo_tools.common_measurement import CommonHarnessPlan, measure_prepared_pair -from research_repo_tools.complete_runs import RunSeries, render_run, run_identity, serialize_run +from research_repo_tools.complete_runs import CompletePolicy, RunSeries, render_run, run_identity, serialize_run +from research_repo_tools.criterion import parse_comparison from research_repo_tools.just_inspect import dry_run -from research_repo_tools.process import run_command +from research_repo_tools.legacy_evidence import convert_csv +from research_repo_tools.process import ExecutableNotFoundError, run_command from research_repo_tools.release_pairs import ReleasePair +import archive_performance as plot import archive_performance as workflow -import criterion_dim_plot as plot import performance_runs as policy -from bench_compare import render_release_artifacts -from performance_artifacts import ArtifactPaths -from release_baseline import required_report_ids +from performance_runs import required_report_ids if TYPE_CHECKING: from research_repo_tools.evidence import Evidence + from research_repo_tools.publication import PublicationPlan ROOT = Path(__file__).resolve().parents[2] PAIR = ReleasePair("v0.4.6", "v0.4.5") @@ -59,7 +62,7 @@ def prepared(tmp_path: Path, fault: str = "") -> tuple[Path, Path, CommonHarness """Use the real consumer inventory and policies with a bounded timing fixture.""" ids = tuple(sorted(required_report_ids())) inventories = { - "baseline": tuple(name for name in ids if not name.startswith(policy.UNAVAILABLE_PREFIXES)), + "baseline": tuple(name for name in ids if not name.startswith(policy.scientific_policy().unavailable_prefixes)), "current": tuple(name for name in ids if not name.startswith("d") or "/la_stack_" in name), } for phase in ("baseline", "current"): @@ -115,6 +118,7 @@ def measured(tmp_path: Path) -> tuple[Path, Evidence]: evidence = measure_prepared_pair(baseline, current, plan, PAIR, working_tree=True) (current / "tooling").mkdir(exist_ok=True) shutil.copyfile(ROOT / policy.REPORT_CONFIG, current / policy.REPORT_CONFIG) + shutil.copyfile(ROOT / workflow.PLOT_CONFIG, current / workflow.PLOT_CONFIG) return current, evidence @@ -132,16 +136,6 @@ def complete_run(tmp_path: Path, measured_run: tuple[Path, Evidence]) -> tuple[P return destination, evidence -@pytest.fixture -def legacy_artifacts(tmp_path: Path) -> ArtifactPaths: - """Keep historical promotion coverage on the last published CSV schema.""" - history = ROOT / "docs/performance" - run = json.loads((history / "latest.json").read_bytes())["run"] - for name in ("performance.csv", "performance.provenance.json"): - shutil.copyfile(history / run / name, tmp_path / name) - return ArtifactPaths(tmp_path / "performance.csv", tmp_path / "performance.provenance.json") - - def test_common_harness_and_reference_phase(tmp_path: Path) -> None: baseline, current, plan = prepared(tmp_path) (baseline / "benches/exact.rs").write_text("obsolete harness\n", encoding="utf-8") @@ -179,10 +173,18 @@ def test_shared_retention_repeated_runs_and_offline_report(complete_run: tuple[P ) stem = root / "target/bench-reports/performance" workflow.save_scratch(root, stem, first) - assert stem.with_suffix(".md").read_bytes() == render_run(first) + assert stem.with_suffix(".md").read_bytes() == render_run( + first, + report_path="target/bench-reports/performance.md", + payload_path="target/bench-reports/performance.run.json", + manifest_path="target/bench-reports/performance.evidence.json", + ) workflow.promote(root, stem) first_report = (root / report_path).read_bytes() - assert first_report == render_run(first, title="Configured performance report") + assert b"Configured performance report" in first_report + assert b"Recorded measurement provenance" in first_report + assert b"run.json" in first_report + assert b"evidence.json" in first_report assert b"## Mean (ns)" in first_report assert b"## Median (ns)" in first_report assert b"nalgebra: measured in **baseline** phase" in first_report @@ -212,63 +214,6 @@ def test_shared_retention_repeated_runs_and_offline_report(complete_run: tuple[P policy.retained_run(root, stem) -@pytest.mark.parametrize("destination", ["missing", "identical", "different", "directory", "symlink", "relative-symlink"]) -def test_legacy_promotion_preserves_prior_report_on_archive_collision(tmp_path: Path, legacy_artifacts: ArtifactPaths, destination: str) -> None: - root = tmp_path - stem = root / "target/bench-reports/performance" - current = root / "docs/performance.md" - current.parent.mkdir(parents=True, exist_ok=True) - previous = b"**la-stack** v0.4.5\n\nComparison against baseline **v0.4.4**:\n\nPrior curated report.\n" - current.write_bytes(previous) - archived = root / "docs/archive/performance/v0.4.5-vs-v0.4.4.md" - archived.parent.mkdir(parents=True) - if destination in {"identical", "different"}: - archived.write_bytes(previous if destination == "identical" else b"Conflicting archive.\n") - elif destination == "directory": - archived.mkdir() - elif destination == "symlink": - archived.symlink_to(current) - elif destination == "relative-symlink": - archived.symlink_to(os.path.relpath(current, archived.parent)) - original_link_target = archived.readlink() if archived.is_symlink() else None - before = {path: path.read_bytes() for path in root.rglob("*") if path.is_file() and ".git" not in path.parts} - if destination in {"missing", "identical"}: - workflow.render_and_promote_artifacts(artifacts=legacy_artifacts, output=stem.with_suffix(".md"), current=current, archive_dir=archived.parent) - assert archived.read_bytes() == previous - assert current.read_text(encoding="utf-8") == render_release_artifacts(legacy_artifacts) - else: - errors = {"different": "immutable output differs", "directory": "regular file", "symlink": "symlink", "relative-symlink": "symlink"} - with pytest.raises(ValueError, match=errors[destination]): - workflow.render_and_promote_artifacts(artifacts=legacy_artifacts, output=stem.with_suffix(".md"), current=current, archive_dir=archived.parent) - assert {path: path.read_bytes() for path in root.rglob("*") if path.is_file() and ".git" not in path.parts} == before - assert not (root / "docs/performance-runs").exists() - if destination == "directory": - assert archived.is_dir() - elif destination in {"symlink", "relative-symlink"}: - assert archived.is_symlink() - assert archived.readlink() == original_link_target - assert archived.samefile(current) - - -@pytest.mark.parametrize("duplicate", ["**la-stack** v0.4.4", "Comparison against baseline **v0.4.3**:"]) -def test_duplicate_legacy_report_identity_prevents_publication(tmp_path: Path, legacy_artifacts: ArtifactPaths, duplicate: str) -> None: - root = tmp_path - stem = root / "target/bench-reports/performance" - current = root / "docs/performance.md" - current.parent.mkdir(parents=True, exist_ok=True) - text = f"**la-stack** v0.4.5\n\nComparison against baseline **v0.4.4**:\n\n{duplicate}\n" - current.write_text(text, encoding="utf-8", newline="\n") - before = {path: path.read_bytes() for path in root.rglob("*") if path.is_file() and ".git" not in path.parts} - with pytest.raises(ValueError, match="exactly one"): - workflow.parse_report_id(text) - with pytest.raises(ValueError, match="exactly one"): - workflow.render_and_promote_artifacts( - artifacts=legacy_artifacts, output=stem.with_suffix(".md"), current=current, archive_dir=root / "docs/archive/performance" - ) - assert {path: path.read_bytes() for path in root.rglob("*") if path.is_file() and ".git" not in path.parts} == before - assert not (root / "docs/performance-runs").exists() - - def test_partial_scratch_and_stale_source_cannot_select_old_history(tmp_path: Path) -> None: root, evidence = measured(tmp_path) stem = root / "target/bench-reports/performance" @@ -283,10 +228,7 @@ def test_partial_scratch_and_stale_source_cannot_select_old_history(tmp_path: Pa @pytest.mark.parametrize("archive", ["docs/performance-runs", "docs/performance-history"]) -@pytest.mark.parametrize("csv_path", ["relative", "dot-relative", "absolute"]) -def test_readme_uses_retained_reference_estimates_after_cleanup( - complete_run: tuple[Path, Evidence], monkeypatch: pytest.MonkeyPatch, archive: str, csv_path: str -) -> None: +def test_readme_uses_original_reference_phases_after_cleanup(complete_run: tuple[Path, Evidence], monkeypatch: pytest.MonkeyPatch, archive: str) -> None: root, evidence = complete_run config = root / policy.REPORT_CONFIG config.write_text(config.read_text(encoding="utf-8").replace("docs/performance-runs", archive), encoding="utf-8") @@ -294,77 +236,220 @@ def test_readme_uses_retained_reference_estimates_after_cleanup( workflow.save_scratch(root, stem, evidence) workflow.promote(root, stem) shutil.copyfile(ROOT / "README.md", root / "README.md") - shutil.rmtree(root / "target") monkeypatch.setattr(plot, "_repo_root", lambda: root) - monkeypatch.setattr(plot, "_render_svg_with_gnuplot", lambda request: request.out_svg.write_bytes(b"\n")) - performance_csv = "target/bench-reports/performance.csv" - if csv_path == "absolute": - performance_csv = str(root / performance_csv) - elif csv_path == "dot-relative": - performance_csv = f"./{performance_csv}" - arguments = ["--update-readme", "--log-y", "--performance-csv", performance_csv] - assert plot.main(arguments) == 0 + monkeypatch.chdir(root.parent) + arguments = ["--update-readme", "--log-y"] + assert plot.dimension_main(arguments) == 0 assets = root / "docs/assets/bench" csv = assets / "vs_linalg_lu_solve_median.csv" - assert "2,8.0,7.2,8.8,10.0,9.0,11.0,10.0,9.0,11.0" in csv.read_text(encoding="utf-8") + import csv as csv_module # noqa: PLC0415 + + with csv.open(encoding="utf-8", newline="") as stream: + rows = list(csv_module.DictReader(stream)) + assert len(rows) == 24 + assert {row["phase"] for row in rows if row["series"] in {"nalgebra", "faer"}} == {"baseline"} + assert {row["phase"] for row in rows if row["series"] == "la-stack current"} == {"current"} + assert {float(row["coordinate"]) for row in rows} == {2, 3, 4, 5, 8, 16, 32, 64} + assert {row["unit"] for row in rows} == {"ns"} + dimension_two = [row for row in rows if float(row["coordinate"]) == 2] + assert [(float(row["point"]), float(row["lower"]), float(row["upper"])) for row in dimension_two] == [(8.0, 7.2, 8.8), (10.0, 9.0, 11.0), (10.0, 9.0, 11.0)] provenance = json.loads(csv.with_suffix(".provenance.json").read_bytes()) - assert provenance["measurement"]["peer_phase"] == "baseline" + assert provenance["series"]["nalgebra"] == "baseline" assert provenance["run_id"] == run_identity(evidence) + assert b" None: root, evidence = complete_run stem = root / "target/bench-reports/performance" workflow.save_scratch(root, stem, evidence) workflow.promote(root, stem) - archive = root / policy.archive_directory(root) - retained = archive / "runs" / run_identity(evidence) - payload, manifest = policy.scratch_paths(stem) - if cleanup: - shutil.rmtree(root / "target") - payload, manifest = retained / "run.json", retained / "evidence.json" - alias = root / "docs/alias.csv" - if destination == "alias": - alias.hardlink_to(payload) - targets = { - "run": payload, - "evidence": manifest, - "alias": alias, - "source": root / "src/lib.rs", - "harness": root / "tooling/performance.just", - "configuration": root / policy.REPORT_CONFIG, - "report": retained / "report.md", - "index": archive / "index.json", - "latest": archive / "latest.json", - } - shutil.copyfile(ROOT / "README.md", root / "docs/custom.md") - before = {path: path.read_bytes() for path in root.rglob("*") if path.is_file() and ".git" not in path.parts} + shutil.copyfile(ROOT / "README.md", root / "README.md") + assets = root / "docs/assets/bench" + assets.mkdir(parents=True) + for suffix in (".svg", ".csv", ".provenance.json"): + (assets / f"vs_linalg_lu_solve_median{suffix}").write_bytes(b"previous artifact\n") + before = {path: path.read_bytes() for path in (*assets.iterdir(), root / "README.md")} + + def fail_git(*_args: object, **_kwargs: object) -> None: + raise error + + monkeypatch.setattr(publication, "run_git_bytes", fail_git) monkeypatch.setattr(plot, "_repo_root", lambda: root) - monkeypatch.setattr(plot, "_render_svg_with_gnuplot", lambda request: request.out_svg.write_bytes(b"\n")) - assert plot.main(["--update-readme", "--readme", "docs/custom.md", "--csv", str(targets[destination])]) == 2 - assert "must use distinct paths" in capsys.readouterr().err - assert {path: path.read_bytes() for path in root.rglob("*") if path.is_file() and ".git" not in path.parts} == before + assert plot.dimension_main(["--update-readme"]) == 2 + output = capsys.readouterr() + assert output.out == "" + assert output.err.startswith("dimension publication:") + assert diagnostic in output.err + assert "Traceback" not in output.err + assert {path: path.read_bytes() for path in (*assets.iterdir(), root / "README.md")} == before + + +def test_stale_source_prevents_dimension_publication(complete_run: tuple[Path, Evidence], monkeypatch: pytest.MonkeyPatch) -> None: + root, evidence = complete_run + stem = root / "target/bench-reports/performance" + workflow.save_scratch(root, stem, evidence) + shutil.copyfile(ROOT / "README.md", root / "README.md") + original = (root / "README.md").read_bytes() + (root / "src/lib.rs").write_text("// changed library\n", encoding="utf-8") + monkeypatch.setattr(plot, "_repo_root", lambda: root) + assert plot.dimension_main(["--update-readme"]) == 2 + assert (root / "README.md").read_bytes() == original + assert not (root / "docs/assets/bench").exists() + + +@pytest.mark.parametrize("retained", [False, True]) +def test_readme_requires_matching_retained_evidence( + complete_run: tuple[Path, Evidence], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], retained: bool +) -> None: + root, evidence = complete_run + stem = root / "target/bench-reports/performance" + workflow.save_scratch(root, stem, evidence) + if retained: + workflow.promote(root, stem) + sources = tuple((phase, replace(source, context=(*source.context, ("experiment", "unpromoted")))) for phase, source in evidence.sources) + workflow.save_scratch(root, stem, replace(evidence, sources=sources)) + shutil.copyfile(ROOT / "README.md", root / "README.md") + before = {path: path.read_bytes() for path in (root / "docs").rglob("*") if path.is_file()} if retained else {} + before[root / "README.md"] = (root / "README.md").read_bytes() + monkeypatch.setattr(plot, "_repo_root", lambda: root) + + assert plot.dimension_main(["--update-readme"]) == 2 + + output = capsys.readouterr() + assert output.out == "" + assert ("README evidence differs from the retained latest run" if retained else "no retained latest run") in output.err + assert {path: path.read_bytes() for path in before} == before + assert not (root / "docs/assets/bench").exists() + + +def test_readme_edit_during_rendering_is_preserved(complete_run: tuple[Path, Evidence], monkeypatch: pytest.MonkeyPatch) -> None: + root, evidence = complete_run + workflow.save_scratch(root, root / "target/bench-reports/performance", evidence) + workflow.promote(root, root / "target/bench-reports/performance") + shutil.copyfile(ROOT / "README.md", root / "README.md") + document = root / "README.md" + edited = document.read_bytes() + b"\nConcurrent maintainer edit.\n" + planner = workflow.plan_outputs + + def edit_before_snapshot(directory: Path, outputs: dict[str, bytes], *, inputs: dict[str, bytes]) -> PublicationPlan: + document.write_bytes(edited) + return planner(directory, outputs, inputs=inputs) + + monkeypatch.setattr(workflow, "plan_outputs", edit_before_snapshot) + with pytest.raises(ValueError, match="README changed during rendering"): + workflow.publish_plot(root, "lu_solve", "median", log_y=True, update_readme=True) + assert document.read_bytes() == edited + assert not (root / "docs/assets/bench").exists() + + +def test_existing_tag_cannot_receive_different_measured_assets(complete_run: tuple[Path, Evidence]) -> None: + root, evidence = complete_run + workflow.save_scratch(root, root / "target/bench-reports/performance", evidence) + workflow.promote(root, root / "target/bench-reports/performance") + shutil.copyfile(ROOT / "README.md", root / "README.md") + assets = root / "docs/assets/bench" + assets.mkdir(parents=True) + for suffix in (".svg", ".csv", ".provenance.json"): + (assets / f"vs_linalg_lu_solve_median{suffix}").write_bytes(b"previous tagged artifact\n") + run_command("git", ["--no-pager", "add", "docs/assets/bench"], cwd=root) + run_command( + "git", + [ + "--no-pager", + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--quiet", + "-m", + "previous artifacts", + ], + cwd=root, + ) + run_command("git", ["--no-pager", "-c", "tag.gpgsign=false", "tag", "v0.4.6"], cwd=root) + originals = {path: path.read_bytes() for path in (*assets.iterdir(), root / "README.md")} + with pytest.raises(ValueError, match="exact publication artifact"): + workflow.publish_plot(root, "lu_solve", "median", log_y=True, update_readme=True) + assert {path: path.read_bytes() for path in originals} == originals + + +@pytest.mark.parametrize("scratch_present", [False, True]) +@pytest.mark.parametrize("changed", ["source-membership", "unindexed-run", "selected-evidence"]) +def test_dimension_publication_rechecks_membership_and_retained_selection( + complete_run: tuple[Path, Evidence], monkeypatch: pytest.MonkeyPatch, changed: str, scratch_present: bool +) -> None: + root, evidence = complete_run + stem = root / "target/bench-reports/performance" + workflow.save_scratch(root, stem, evidence) + workflow.promote(root, stem) + shutil.copyfile(ROOT / "README.md", root / "README.md") + if not scratch_present: + shutil.rmtree(root / "target") + original = (root / "README.md").read_bytes() + publish = workflow.publish_publication + + def change_before_publication(plan: PublicationPlan) -> tuple[Path, ...]: + if changed == "source-membership": + (root / "src/added.rs").write_text("// added after planning\n", encoding="utf-8") + elif changed == "selected-evidence": + # Scratch is an input only while present; the archive remains the + # selected source after cleanup. + if scratch_present: + policy.scratch_paths(stem)[0].write_bytes(b"{}\n") + else: + (root / policy.archive_directory(root) / "runs" / run_identity(evidence) / "run.json").write_bytes(b"{}\n") + else: + unexpected = root / policy.archive_directory(root) / "runs" / ("f" * 64) + unexpected.mkdir() + (unexpected / "run.json").write_bytes(b"{}\n") + return publish(plan) + + monkeypatch.setattr(workflow, "_repo_root", lambda: root) + monkeypatch.setattr(workflow, "publish_publication", change_before_publication) + assert workflow.dimension_main(["--update-readme"]) == 2 + assert (root / "README.md").read_bytes() == original + assert not (root / "docs/assets/bench").exists() def test_complete_report_passes_markdown_recipes_without_rewriting_history(complete_run: tuple[Path, Evidence]) -> None: @@ -394,15 +479,26 @@ def test_complete_report_passes_markdown_recipes_without_rewriting_history(compl def test_legacy_bytes_hashes_and_report_are_unchanged() -> None: directory = ROOT / "docs/performance/v0.4.6-vs-v0.4.5/39719a1eb599d294dbbc1e46dc6fb0de2028ad2e59aab5f94fb48c7bf3d7f841" - paths = ArtifactPaths(directory / "performance.csv", directory / "performance.provenance.json") hashes = { "performance.csv": "b263bb34bcfd43b8fc8109110db9f2e908516aef6c01ce16f70f5ac18dc7ef2d", "performance.full.csv": "4fa5cc13159f5dc91e711ae67554845bcbd1ace2cfc3e31487fc85f75d3dc459", "performance.full.provenance.json": "a43f66e52035aecd8d235dfded916b23c3240529d0c07f793d27322521cfb0d0", "performance.provenance.json": "89144a2899ba73f55d009da4fa20b4e6477600902b93cde469383f719c497508", } - assert {name: hashlib.sha256((paths.csv.parent / name).read_bytes()).hexdigest() for name in hashes} == hashes - assert hashlib.sha256(render_release_artifacts(paths).encode()).hexdigest() == "4ea0ff9bca4c7bb92cd57bd9163b3cacabe78f502abdd2663162cf9562ec6582" + assert {name: hashlib.sha256((directory / name).read_bytes()).hexdigest() for name in hashes} == hashes + + retained = convert_csv( + (directory / "performance.csv").read_bytes(), + (directory / "performance.provenance.json").read_bytes(), + (ROOT / "tooling/performance-legacy.toml").read_bytes(), + ) + comparison = parse_comparison(retained.payload) + assert len(comparison.comparisons) == 190 + assert len(comparison.missing_baseline) == 35 + assert not comparison.missing_current + assert "rational_input_d8/solve_big_rational_gaussian" in comparison.missing_baseline + assert dict(retained.sources)["baseline"].source_sha256 is None + assert dict(dict(retained.sources)["baseline"].context)["legacy.payload-sha256"] == hashes["performance.csv"] def test_native_command_policy_and_compatibility_adapter() -> None: @@ -470,8 +566,11 @@ def test_native_inventory_and_phase_commands_use_the_measured_checkout(tmp_path: record = tmp_path / "commands.txt" monkeypatch.setenv("PATH", f"{binary}{os.pathsep}{os.environ['PATH']}") monkeypatch.setenv("CARGO_RECORD", str(record)) + monkeypatch.chdir(tmp_path) pair = ReleasePair("v0.4.6", baseline_tag) - plan = policy.measurement_plan(root, pair, "all", scope, tmp_path / "inventory") + plan = policy.measurement_plan(root, pair, "all", scope) + assert isinstance(plan.current.policy, CompletePolicy) + assert isinstance(plan.baseline.policy, CompletePolicy) assert set(plan.current.policy.expected) == set(current_ids) | set(exact) unavailable = norm_rows | {name for name in exact if name.startswith(("rational_input_", "canonical_conversion_", "det4_diagnostic_"))} assert set(plan.baseline.policy.expected) == (ids if baseline_tag == "v0.4.6" else ids - unavailable) @@ -529,7 +628,8 @@ def test_pre_just_run_remains_readable_without_its_python_driver(tmp_path: Path) policy.validate_scientific_run(replace(legacy, sources=(("baseline", bad), sources[1]))) -def test_local_explicit_labels_and_release_eligibility() -> None: +def test_local_explicit_labels_and_release_eligibility(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) assert workflow.select_pair(ROOT, "explicit", "v0.4.6", "v0.4.6", local=True) == ReleasePair("v0.4.6", "v0.4.6") with pytest.raises(ValueError, match="must differ"): workflow.select_pair(ROOT, "explicit", "v0.4.6", "v0.4.6") @@ -537,6 +637,122 @@ def test_local_explicit_labels_and_release_eligibility() -> None: workflow.select_pair(ROOT, "explicit", "v0.4.6", "v0.4.3", local=True) +@pytest.mark.parametrize( + ("field", "value"), + [ + ("minimum-release", "[]"), + ("minimum-release", "[0, 4]"), + ("minimum-release", "[0, 4, true]"), + ("minimum-release", "[0, 4, -1]"), + ("modern-api-release", "[]"), + ("modern-api-release", "[0, 4, 3]"), + ("legacy-api-cfg", '"--arbitrary-flag"'), + ], +) +def test_malformed_release_policy_is_rejected(tmp_path: Path, field: str, value: str) -> None: + directory = tmp_path / "tooling" + directory.mkdir() + text = (ROOT / policy.POLICY_CONFIG).read_text(encoding="utf-8") + import re # noqa: PLC0415 + + text = re.sub(rf"(?m)^{re.escape(field)} = .*$", f"{field} = {value}", text) + (tmp_path / policy.POLICY_CONFIG).write_text(text, encoding="utf-8") + with pytest.raises(ValueError, match=r"release|cfg"): + policy.compatibility_adapter(PAIR, tmp_path) + + +@pytest.mark.parametrize( + ("field", "value", "diagnostic"), + [ + ("dimensions", "[]", "dimensions"), + ("dimensions", "[2, true]", "dimensions"), + ("dimensions", "[2, -3]", "dimensions"), + ("dimensions", "[3, 2]", "dimensions"), + ("dimensions", "[2, 2]", "dimensions"), + ("library-benchmarks", '"la_stack_lu"', "library-benchmarks"), + ("library-benchmarks", '["la_stack_lu", "la_stack_lu"]', "library-benchmarks"), + ("unavailable-prefixes", "[false]", "unavailable-prefixes"), + ("unavailable-benchmarks", '["../escape"]', "unavailable-benchmarks"), + ("unavailable-benchmark-prefixes", "[1]", "unavailable-benchmark-prefixes"), + ("exact_d2", '"det"', "exact.exact_d2"), + ('"8"', "[false]", "additional.8"), + ("la_stack_lu", '["nalgebra_lu"]', "peers.la_stack_lu"), + ("la_stack_lu", '["nalgebra_lu", "faer_lu", "extra"]', "peers.la_stack_lu"), + ("la_stack_lu", '["nalgebra_lu", "nalgebra_lu"]', "peers.la_stack_lu"), + ("title", '" "', "metrics.det_via_lu.title"), + ("benchmarks", '["la_stack_det_via_lu", "nalgebra_det_via_lu"]', "metrics.det_via_lu.benchmarks"), + ("benchmarks", '["la_stack_det_via_lu", "nalgebra_det_via_lu", "wrong_peer"]', "declared peer pair"), + ], +) +def test_malformed_scientific_catalog_is_rejected(tmp_path: Path, field: str, value: str, diagnostic: str) -> None: + """Bad declarations fail at admission, before inventory indexing or timing.""" + import re # noqa: PLC0415 + + (tmp_path / "tooling").mkdir() + text = (ROOT / policy.POLICY_CONFIG).read_text(encoding="utf-8") + text, count = re.subn(rf"(?m)^{re.escape(field)} = .*$", f"{field} = {value}", text, count=1) + assert count == 1 + (tmp_path / policy.POLICY_CONFIG).write_text(text, encoding="utf-8") + with pytest.raises(ValueError, match=re.escape(diagnostic)): + policy.compatibility_adapter(PAIR, tmp_path) + + +@pytest.mark.parametrize( + ("original", "replacement", "diagnostic"), + [ + ("[exact]", "[renamed_exact]", "exact requires a named catalog table"), + ('"8" =', '"7" =', "additional benchmark dimensions"), + ("la_stack_lu =", "unknown_benchmark =", "peers must refer"), + ("[metrics.det_via_lu]", "[metrics]", "metrics.title requires a named catalog table"), + ], +) +def test_malformed_scientific_tables_are_rejected(tmp_path: Path, original: str, replacement: str, diagnostic: str) -> None: + """Missing tables and cross-catalog inconsistencies retain field diagnostics.""" + (tmp_path / "tooling").mkdir() + text = (ROOT / policy.POLICY_CONFIG).read_text(encoding="utf-8") + assert original in text + (tmp_path / policy.POLICY_CONFIG).write_text(text.replace(original, replacement, 1), encoding="utf-8") + with pytest.raises(ValueError, match=diagnostic): + policy.scientific_policy(tmp_path) + + +def test_parsed_scientific_catalog_preserves_immutable_peer_and_metric_rows() -> None: + """Shared planners consume stable, typed catalog rows rather than raw TOML.""" + catalog = policy.scientific_policy(ROOT) + assert catalog.dimensions == (2, 3, 4, 5, 8, 16, 32, 64) + assert catalog.peers["la_stack_lu"] == ("nalgebra_lu", "faer_lu") + assert catalog.metrics["lu"].benchmarks == ("la_stack_lu", "nalgebra_lu", "faer_lu") + from typing import cast # noqa: PLC0415 + + with pytest.raises(TypeError): + cast("dict[str, tuple[str, str]]", catalog.peers)["la_stack_lu"] = ("wrong", "peer") + + +@pytest.mark.parametrize("flag", [("--suite", "exact"), ("--scope", "all-benches"), ("--stem", "ignored")]) +def test_assets_rejects_retired_options(flag: tuple[str, str], capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit) as error: + workflow.main(["assets", *flag]) + assert error.value.code == 2 + assert "unrecognized arguments" in capsys.readouterr().err + + +@pytest.mark.parametrize("command", ["plot", "local"]) +def test_cli_help_and_missing_checkout_diagnostics(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], command: str) -> None: + monkeypatch.chdir(tmp_path) + cli = workflow.dimension_main if command == "plot" else workflow.main + arguments = [] if command == "plot" else [command] + with pytest.raises(SystemExit) as error: + cli([*arguments, "--help"]) + assert error.value.code == 0 + output = capsys.readouterr() + assert "usage:" in output.out + assert not output.err + assert cli(arguments) == (2 if command == "plot" else 1) + output = capsys.readouterr() + assert "Could not locate Cargo.toml" in output.err + assert "Traceback" not in output.err + + def test_current_gate_failure_prevents_both_timing_phases(tmp_path: Path) -> None: baseline, current, plan = prepared(tmp_path) plan = replace(plan, current=replace(plan.current, environment=(("FIXTURE_FAULT", "gate"),))) @@ -576,34 +792,6 @@ def test_reference_phase_and_same_version_policy(tmp_path: Path) -> None: policy.validate_scientific_run(identical) -def test_publication_io_failure_preserves_all_published_outputs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - root, evidence = measured(tmp_path) - stem = root / "target/bench-reports/performance" - workflow.save_scratch(root, stem, evidence) - workflow.promote(root, stem) - originals = {path: path.read_bytes() for path in (root / "docs").rglob("*") if path.is_file()} - second = replace( - evidence, sources=tuple((phase, replace(source, context=(*source.context, ("experiment", "second")))) for phase, source in evidence.sources) - ) - workflow.save_scratch(root, stem, second) - native_replace = Path.replace - failed = False - - def fail_once(source: Path, destination: Path) -> Path: - nonlocal failed - if destination == root / policy.archive_directory(root) / "latest.json" and not failed: - failed = True - msg = "injected publication failure" - raise OSError(msg) - return native_replace(source, destination) - - monkeypatch.setattr(Path, "replace", fail_once) - with pytest.raises(OSError, match="injected publication failure"): - workflow.promote(root, stem) - assert failed - assert {path: path.read_bytes() for path in (root / "docs").rglob("*") if path.is_file()} == originals - - def test_unsafe_release_asset_cannot_replace_report(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: output = tmp_path / "report.md" output.write_bytes(b"previous report\n") @@ -616,5 +804,25 @@ def unsafe_asset(_root: Path, _repo: str, _tag: str, _name: str, destination: Pa monkeypatch.setattr(workflow, "download_release_asset", unsafe_asset) with pytest.raises(ValueError, match=r"unsafe|traversal|relative|member"): - workflow.compare_assets(tmp_path, PAIR, output, "all", "release-signal") + workflow.compare_assets(tmp_path, PAIR, output) assert output.read_bytes() == b"previous report\n" + + +def test_shared_saved_comparison_recipe_reports_coverage(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Exercise the actual recipe and installed CLI, with no consumer package.""" + for name in ("justfile", "pyproject.toml", "uv.lock", ".python-version", "rust-toolchain.toml"): + shutil.copyfile(ROOT / name, tmp_path / name) + for name, sample, value in (("workload/shared", "last", 10), ("workload/shared", "new", 8), ("diagnostic/new", "new", 3)): + path = tmp_path / "target/criterion" / name / sample + path.mkdir(parents=True) + (path / "benchmark.json").write_text(json.dumps({"full_id": name}), encoding="utf-8") + estimate = {"point_estimate": value, "confidence_interval": {"lower_bound": value * 0.9, "upper_bound": value * 1.1}} + (path / "estimates.json").write_text(json.dumps({"median": estimate}), encoding="utf-8") + monkeypatch.setenv("UV_PROJECT_ENVIRONMENT", sys.prefix) + result = run_command("just", ["--no-deps", "bench-compare", "last"], cwd=tmp_path, check=False) + assert result.returncode == 0, result.stdout + result.stderr + report = (tmp_path / "target/bench-reports/performance.md").read_text(encoding="utf-8") + assert "workload/shared" in report + assert "diagnostic/new" in report + assert not (tmp_path / "target/bench-reports/performance.csv").exists() + assert not (tmp_path / "target/bench-reports/performance.provenance.json").exists() diff --git a/scripts/tests/test_release_baseline.py b/scripts/tests/test_release_baseline.py index 7410eb4..6b48b93 100644 --- a/scripts/tests/test_release_baseline.py +++ b/scripts/tests/test_release_baseline.py @@ -1,24 +1,28 @@ """Exercise complete release archives and the workflow's publication barriers.""" -import hashlib import json import os import re +import shlex import shutil import subprocess +import sys import tarfile import textwrap from pathlib import Path import pytest +from research_repo_tools.criterion_inventory import CriterionInventory, load_discovery, serialize_inventory -import release_baseline +import archive_performance as workflow +from performance_runs import required_report_ids REPO_ROOT = Path(__file__).resolve().parents[2] WORKFLOW = REPO_ROOT / ".github/workflows/release-benchmarks.yml" PREPARATION = REPO_ROOT / ".github/actions/prepare-release-benchmarks/action.yml" IDS = {"vs_linalg": ["d2/la_stack_dot", "diagnostic/peer"], "exact": ["exact_d2/det_exact"]} BASELINE = "v1.2.3" +RAW_FILES = ("benchmark.json", "estimates.json", "sample.json", "tukey.json") def write_json(path: Path, data: object) -> None: @@ -28,10 +32,18 @@ def write_json(path: Path, data: object) -> None: @pytest.fixture def dataset(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[Path, Path]: - monkeypatch.setattr(release_baseline, "required_report_ids", lambda: {"d2/la_stack_dot", "exact_d2/det_exact"}) + monkeypatch.chdir(tmp_path) + (tmp_path / "Cargo.toml").write_text('[package]\nname = "fixture"\nversion = "1.2.3"\n', encoding="utf-8") + tooling = tmp_path / "tooling" + tooling.mkdir() + shutil.copyfile(REPO_ROOT / "tooling/criterion-saved.toml", tooling / "criterion-saved.toml") + (tooling / "criterion-inventory.toml").write_text( + 'schema = 1\nrequired = ["d2/la_stack_dot", "exact_d2/det_exact"]\n[[suites]]\nname = "release"\ncommand = ["unused"]\n', encoding="utf-8" + ) criterion = tmp_path / "target/criterion" manifest = tmp_path / "target/release-benchmark-inventory.json" - write_json(manifest, IDS) + manifest.parent.mkdir(parents=True) + manifest.write_bytes(serialize_inventory(CriterionInventory(tuple((name, tuple(ids)) for name, ids in IDS.items())))) estimate = {"point_estimate": 2.0, "confidence_interval": {"confidence_level": 0.95, "lower_bound": 1.0, "upper_bound": 3.0}} for ids in IDS.values(): for benchmark in ids: @@ -72,8 +84,8 @@ def run_shell(script: str, cwd: Path, extra_env: dict[str, str]) -> subprocess.C def test_complete_dataset_round_trips_through_workflow_archive(dataset: tuple[Path, Path], tmp_path: Path) -> None: - criterion, manifest = dataset - assert release_baseline.validate(criterion, manifest, BASELINE) == 3 + criterion, _manifest = dataset + assert workflow.main(["release-check", BASELINE]) == 0 output = tmp_path / "step outputs" # Git Bash accepts forward-slash drive paths on Windows as well as POSIX paths. result = run_shell( @@ -88,112 +100,55 @@ def test_complete_dataset_round_trips_through_workflow_archive(dataset: tuple[Pa for ids in IDS.values(): for benchmark in ids: for sample in ("new", BASELINE): - for filename in release_baseline.RAW_FILES: + for filename in RAW_FILES: member = archive.extractfile(f"criterion/{benchmark}/{sample}/{filename}") assert member is not None assert member.read() == (criterion / benchmark / sample / filename).read_bytes() member = archive.extractfile("criterion/release-benchmark-inventory.json") assert member is not None - assert json.loads(member.read()) == IDS - - -@pytest.mark.parametrize("filename", release_baseline.RAW_FILES) -@pytest.mark.parametrize("sample", ["new", BASELINE]) -def test_missing_raw_file_blocks_publication(dataset: tuple[Path, Path], sample: str, filename: str) -> None: - criterion, manifest = dataset - missing = criterion / "diagnostic/peer" / sample / filename - missing.unlink() - if sample == "new" and filename != "tukey.json": - with pytest.raises(ValueError, match="incomplete Criterion sample"): - release_baseline.validate(criterion, manifest, BASELINE) - else: - with pytest.raises(FileNotFoundError) as error: - release_baseline.validate(criterion, manifest, BASELINE) - assert error.value.filename == str(missing) - - -def test_missing_entire_suite_blocks_publication(dataset: tuple[Path, Path]) -> None: - criterion, manifest = dataset - shutil.rmtree(criterion / "exact_d2") - with pytest.raises(ValueError, match=r"inventory differs: missing=.*exact_d2/det_exact"): - release_baseline.validate(criterion, manifest, BASELINE) + assert json.loads(member.read())["suites"] == IDS def test_saved_baseline_must_match_fresh_samples(dataset: tuple[Path, Path]) -> None: - criterion, manifest = dataset + criterion, _manifest = dataset write_json(criterion / "d2/la_stack_dot" / BASELINE / "sample.json", {"iters": [1.0] * 100, "times": [3.0] * 100}) - with pytest.raises(ValueError, match="saved baseline differs"): - release_baseline.validate(criterion, manifest, BASELINE) + assert workflow.main(["release-check", BASELINE]) == 1 -def test_report_consumers_require_canonical_paths(dataset: tuple[Path, Path]) -> None: - criterion, manifest = dataset - (criterion / "d2/la_stack_dot").rename(criterion / "d2/misplaced") - with pytest.raises(ValueError, match="outside its consumer path"): - release_baseline.validate(criterion, manifest, BASELINE) +@pytest.mark.parametrize("relocation", ["moved", "swapped"]) +def test_report_consumers_require_canonical_paths(dataset: tuple[Path, Path], capsys: pytest.CaptureFixture[str], relocation: str) -> None: + criterion, _manifest = dataset + original = criterion / "d2/la_stack_dot" + moved = original.rename(criterion / "d2/misplaced") + if relocation == "swapped": + peer = criterion / "exact_d2/det_exact" + peer.rename(original) + moved.rename(peer) + before = {path: path.read_bytes() for path in criterion.rglob("*") if path.is_file()} + assert workflow.main(["release-check", BASELINE]) == 1 -@pytest.mark.parametrize("baseline", ["../escape", "", "new", "base", "change", "report", "/absolute"]) -def test_invalid_baseline_is_rejected(dataset: tuple[Path, Path], baseline: str) -> None: - with pytest.raises(ValueError, match="safe, nonreserved"): - release_baseline.validate(*dataset, baseline) + output = capsys.readouterr() + assert output.out == "" + assert "non-canonical report benchmark storage path" in output.err + assert "d2/la_stack_dot" in output.err + assert {path: path.read_bytes() for path in criterion.rglob("*") if path.is_file()} == before -def test_inventory_includes_release_consumers_and_peers() -> None: - expected = release_baseline.required_report_ids() +def test_configured_inventory_includes_release_consumers_and_peers() -> None: + expected = required_report_ids(REPO_ROOT) assert {"d64/faer_lu_solve", "d64/nalgebra_lu_solve", "exact_hilbert_5x5/solve_exact", "rational_input_d8/solve_big_rational_gaussian"} <= expected assert "d64/faer_dot_native" in expected assert "d64/faer_dot" not in expected - with pytest.raises(ValueError, match="inventory omits report consumers"): - release_baseline.inventory_ids(IDS) - - -@pytest.mark.parametrize("output", ["", "hello", "a/b: benchmark\na/b: benchmark", "a: benchmark"]) -def test_invalid_listing_is_rejected(output: str) -> None: - with pytest.raises(ValueError, match="unique, nonempty"): - release_baseline.parse_benchmark_list(output) - - -@pytest.mark.parametrize("host_newline", ["\n", "\r\n"], ids=["posix", "windows"]) -def test_discovery_uses_full_suites_and_never_times_inputs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, host_newline: str) -> None: - calls: list[list[str]] = [] - write_text = Path.write_text - - def host_write_text(path: Path, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None) -> int: - # Emulate Windows default text translation on every CI host. An explicit - # newline policy must preserve identical manifest bytes on both platforms. - return write_text(path, data, encoding=encoding, errors=errors, newline=host_newline if newline is None else newline) - - def run(command: str, args: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: - assert command == "just" - calls.append(args) - suite = args[-2].removeprefix("baseline-") - return subprocess.CompletedProcess(args, 0, stdout="\n".join(f"{name}: benchmark" for name in IDS[suite])) - - monkeypatch.setattr(release_baseline, "run_command", run) - monkeypatch.setattr(release_baseline, "required_report_ids", lambda: {"d2/la_stack_dot", "exact_d2/det_exact"}) - monkeypatch.setattr(Path, "write_text", host_write_text) - manifest = tmp_path / "manifest.json" - release_baseline.discover(tmp_path, manifest, tmp_path / "criterion") - assert manifest.read_bytes() == (json.dumps(IDS, indent=2) + "\n").encode("utf-8") - assert calls == [ - ["--justfile", "tooling/performance.just", "--working-directory", ".", "baseline-vs_linalg", "list"], - ["--justfile", "tooling/performance.just", "--working-directory", ".", "baseline-exact", "list"], - ] - - -def test_discovery_refuses_stale_measurements(dataset: tuple[Path, Path], tmp_path: Path) -> None: - criterion, manifest = dataset - before = manifest.read_bytes() - with pytest.raises(ValueError, match="fresh Criterion directory"): - release_baseline.discover(tmp_path, manifest, criterion) - assert manifest.read_bytes() == before + discovery = load_discovery(REPO_ROOT, "tooling/criterion-inventory.toml") + assert {suite.name for suite in discovery.suites} == {"vs_linalg", "exact"} + assert all(suite.command[-1] == "list" for suite in discovery.suites) def test_workflow_requires_preflight_and_isolates_publication_permissions() -> None: workflow = WORKFLOW.read_text(encoding="utf-8") preparation = PREPARATION.read_text(encoding="utf-8") - assert preparation.index("- name: Validate benchmark inputs") < preparation.index("- name: Inventory full release suites") + assert preparation.index("- name: Inventory full release suites") < preparation.index("- name: Validate benchmark inputs") assert "run: just test-bench-inputs" in preparation assert "run: just bench-release-inventory" in preparation assert "continue-on-error" not in preparation @@ -228,6 +183,11 @@ def test_workflow_requires_preflight_and_isolates_publication_permissions() -> N assert "persist-credentials: false" in producer assert "contents: write" in preflight assert "actions/checkout@" not in preflight + publisher + assert "target: ${{ steps.target.outputs.target }}" in preflight + assert "RELEASE_TARGET: ${{ needs.validate-release.outputs.target }}" in publisher + assert '--expected-title "$RELEASE_TAG"' in preflight + assert '--target "$RUNNER_TEMP/release-target.json" --publish' in publisher + assert (preflight + publisher).count("--from research-repo-tools==0.1.9") == 2 assert "uses: ./" not in preflight + publisher assert "--clobber" not in workflow assert "needs: [validate-release, release-baseline]" in publisher @@ -285,111 +245,6 @@ def test_failed_suite_retains_timing_and_does_not_mark_completion(tmp_path: Path assert "| exact | skipped |" in summary.read_text(encoding="utf-8") -# Only the GitHub boundary is substituted. jq predicates, checksums, shell -# failure handling, and publication ordering execute from the workflow itself. -GITHUB_STUB = r""" -gh() { - printf '%s\n' "$*" >> api-calls - [[ "$1" == api ]] || return 90 - case "$2" in - 'repos/owner/repo/releases?per_page=100') - [[ "$*" == *'--paginate --slurp' ]] || return 91 - cat releases.json - ;; - "repos/owner/repo/commits/refs/tags/$RELEASE_TAG") - [[ "${COMMIT_STATUS:-0}" == 0 ]] || return "$COMMIT_STATUS" - if [[ -f commit-read ]]; then - printf '%s\n' "${LATER_COMMIT:-$RELEASE_COMMIT}" - else - touch commit-read - printf '%s\n' "$RELEASE_COMMIT" - fi - ;; - repos/owner/repo/releases/42) - if [[ "$*" == *'--method PATCH -F draft=false' ]]; then - touch publish-called - [[ "${PUBLISH_STATUS:-0}" == 0 ]] || return "$PUBLISH_STATUS" - cat published.json - elif [[ -f draft-read ]]; then - cat later-draft.json - else - touch draft-read - cat draft.json - fi - ;; - 'repos/owner/repo/releases/42/assets?per_page=100') - [[ "$*" == *'--paginate --slurp' ]] || return 92 - if [[ -f upload-called ]]; then - cat uploaded-assets.json - else - cat assets.json - fi - ;; - "https://uploads.github.com/repos/owner/repo/releases/42/assets?name=$RELEASE_ASSET") - [[ "$*" == *'--method POST --header Content-Type: application/gzip --input '* ]] || return 93 - touch upload-called - return "${UPLOAD_STATUS:-0}" - ;; - *) return 94 ;; - esac -} -""" - - -def draft_release(**changes: object) -> dict[str, object]: - return {"id": 42, "tag_name": BASELINE, "name": BASELINE, "draft": True, "prerelease": False, "immutable": False, **changes} - - -@pytest.fixture -def github_release(tmp_path: Path) -> dict[str, str]: - asset = f"la-stack-{BASELINE}-criterion-baseline.tar.gz" - payload = b"test archive bytes\n" - (tmp_path / asset).write_bytes(payload) - metadata = {"name": asset, "state": "uploaded", "size": len(payload), "digest": f"sha256:{hashlib.sha256(payload).hexdigest()}"} - write_json(tmp_path / "releases.json", [[draft_release(tag_name="v0.1.0")], [draft_release()]]) - write_json(tmp_path / "draft.json", draft_release()) - write_json(tmp_path / "later-draft.json", draft_release()) - write_json(tmp_path / "published.json", draft_release(draft=False, immutable=True)) - write_json(tmp_path / "assets.json", [[]]) - write_json(tmp_path / "uploaded-assets.json", [[{"name": "unrelated.txt"}], [metadata]]) - return { - "GH_REPO": "owner/repo", - "RELEASE_TAG": BASELINE, - "RELEASE_ID": "42", - "RELEASE_COMMIT": "a" * 40, - "RELEASE_ASSET": asset, - "GITHUB_REF": f"refs/tags/{BASELINE}", - "GITHUB_SHA": "a" * 40, - "GITHUB_OUTPUT": (tmp_path / "outputs").as_posix(), - } - - -def test_preflight_captures_draft_identity_and_peeled_tag_commit(tmp_path: Path, github_release: dict[str, str]) -> None: - result = run_shell(GITHUB_STUB + shell_step("Validate draft release target"), tmp_path, github_release) - assert result.returncode == 0, result.stderr - assert (tmp_path / "outputs").read_text(encoding="utf-8") == f"release-id=42\ncommit={'a' * 40}\n" - calls = (tmp_path / "api-calls").read_text(encoding="utf-8") - assert "--paginate --slurp" in calls - assert f"commits/refs/tags/{BASELINE}" in calls - assert "--method" not in calls - - -@pytest.mark.parametrize("ref", ["refs/heads/main", f"refs/heads/{BASELINE}", "refs/tags/v9.9.9", ""]) -def test_preflight_rejects_dispatch_outside_release_tag(tmp_path: Path, github_release: dict[str, str], ref: str) -> None: - result = run_shell(GITHUB_STUB + shell_step("Validate draft release target"), tmp_path, {**github_release, "GITHUB_REF": ref}) - assert result.returncode == 1 - assert f"Dispatch with --ref {BASELINE}" in result.stdout - assert not (tmp_path / "api-calls").exists() - assert not (tmp_path / "outputs").exists() - - -def test_preflight_rejects_tag_moved_since_dispatch(tmp_path: Path, github_release: dict[str, str]) -> None: - result = run_shell(GITHUB_STUB + shell_step("Validate draft release target"), tmp_path, {**github_release, "RELEASE_COMMIT": "b" * 40}) - assert result.returncode == 1 - assert "Release tag no longer matches the workflow commit" in result.stdout - assert not (tmp_path / "outputs").exists() - - @pytest.mark.parametrize("validated_attempt", ["", "1", "2"]) def test_benchmark_retry_requires_current_preflight(tmp_path: Path, validated_attempt: str) -> None: result = run_shell( @@ -398,133 +253,100 @@ def test_benchmark_retry_requires_current_preflight(tmp_path: Path, validated_at {"VALIDATED_ATTEMPT": validated_attempt, "GITHUB_RUN_ATTEMPT": "2"}, ) assert result.returncode == (0 if validated_attempt == "2" else 1) - if validated_attempt != "2": - assert "Rerun all jobs or dispatch again" in result.stdout -@pytest.mark.parametrize("tag", ["", "1.2.3", "v01.2.3", "v1.02.3", "v1.2.03", "v1.2.3-rc.1", "v1.2.3+meta", "main", "v1.2.3\nextra", "$(touch injected)"]) -def test_preflight_rejects_invalid_tag_before_api_access(tmp_path: Path, github_release: dict[str, str], tag: str) -> None: - result = run_shell(GITHUB_STUB + shell_step("Validate draft release target"), tmp_path, {**github_release, "RELEASE_TAG": tag}) +@pytest.mark.parametrize("ref", ["refs/heads/main", f"refs/heads/{BASELINE}", "refs/tags/v9.9.9", ""]) +def test_preflight_rejects_dispatch_outside_release_tag(tmp_path: Path, ref: str) -> None: + output = tmp_path / "outputs" + result = run_shell(shell_step("Validate draft release target"), tmp_path, {"RELEASE_TAG": BASELINE, "GITHUB_REF": ref, "GITHUB_OUTPUT": output.as_posix()}) assert result.returncode == 1 - assert "Expected an existing stable vX.Y.Z tag" in result.stdout - assert not (tmp_path / "api-calls").exists() - assert not (tmp_path / "outputs").exists() - assert not (tmp_path / "injected").exists() - - -@pytest.mark.parametrize( - "releases", - [ - [], - [draft_release(), draft_release(id=43)], - [draft_release(draft=False)], - [draft_release(prerelease=True)], - [draft_release(immutable=True)], - [draft_release(immutable=None)], - [draft_release(tag_name="v9.9.9")], - [draft_release(name="Wrong title")], - [draft_release(id="42\ncommit=injected")], - ], -) -def test_preflight_rejects_unsuitable_release(tmp_path: Path, github_release: dict[str, str], releases: list[dict[str, object]]) -> None: - write_json(tmp_path / "releases.json", [releases]) - result = run_shell(GITHUB_STUB + shell_step("Validate draft release target"), tmp_path, github_release) - assert result.returncode != 0 - assert "commits/" not in (tmp_path / "api-calls").read_text(encoding="utf-8") - assert not (tmp_path / "outputs").exists() - - -@pytest.mark.parametrize("response", ["{invalid", "null"]) -def test_preflight_rejects_malformed_api_response(tmp_path: Path, github_release: dict[str, str], response: str) -> None: - (tmp_path / "releases.json").write_text(response, encoding="utf-8") - result = run_shell(GITHUB_STUB + shell_step("Validate draft release target"), tmp_path, github_release) - assert result.returncode != 0 - assert not (tmp_path / "outputs").exists() - - -@pytest.mark.parametrize("overrides", [{"COMMIT_STATUS": "4"}, {"RELEASE_COMMIT": ""}, {"RELEASE_COMMIT": "a" * 40 + "\ninjected=true"}]) -def test_missing_or_invalid_tag_commit_stops_preflight(tmp_path: Path, github_release: dict[str, str], overrides: dict[str, str]) -> None: - result = run_shell(GITHUB_STUB + shell_step("Validate draft release target"), tmp_path, {**github_release, **overrides}) - assert result.returncode != 0 - assert not (tmp_path / "outputs").exists() - - -@pytest.mark.parametrize("reuse", [False, True], ids=["fresh-upload", "interrupted-upload-rerun"]) -def test_publisher_verifies_durable_asset_before_publication(tmp_path: Path, github_release: dict[str, str], reuse: bool) -> None: - if reuse: - shutil.copyfile(tmp_path / "uploaded-assets.json", tmp_path / "assets.json") - result = run_shell(GITHUB_STUB + shell_step("Attach baseline and publish draft"), tmp_path, github_release) - assert result.returncode == 0, result.stderr - calls = (tmp_path / "api-calls").read_text(encoding="utf-8").splitlines() - assert calls[-1] == "api repos/owner/repo/releases/42 --method PATCH -F draft=false" - assert calls[-2] == "api repos/owner/repo/releases/42/assets?per_page=100 --paginate --slurp" - assert calls.count("api repos/owner/repo/releases/42") == 2 - assert (tmp_path / "upload-called").exists() is not reuse - assert (tmp_path / "publish-called").exists() - assert "--clobber" not in " ".join(calls) - - -@pytest.mark.parametrize("snapshot", ["draft.json", "later-draft.json"]) -@pytest.mark.parametrize( - "changes", - [{"draft": False}, {"prerelease": True}, {"immutable": True}, {"tag_name": "v9.9.9"}, {"name": "renamed"}, {"id": 43}], -) -def test_changed_release_stops_publication(tmp_path: Path, github_release: dict[str, str], snapshot: str, changes: dict[str, object]) -> None: - write_json(tmp_path / snapshot, draft_release(**changes)) - result = run_shell(GITHUB_STUB + shell_step("Attach baseline and publish draft"), tmp_path, github_release) - assert result.returncode != 0 - assert not (tmp_path / "publish-called").exists() - assert (tmp_path / "upload-called").exists() is (snapshot == "later-draft.json") - - -@pytest.mark.parametrize("after_upload", [False, True]) -def test_moved_tag_stops_publication(tmp_path: Path, github_release: dict[str, str], after_upload: bool) -> None: - overrides = {"LATER_COMMIT": "b" * 40} - if not after_upload: - (tmp_path / "commit-read").touch() - result = run_shell(GITHUB_STUB + shell_step("Attach baseline and publish draft"), tmp_path, {**github_release, **overrides}) - assert result.returncode != 0 - assert not (tmp_path / "publish-called").exists() - assert (tmp_path / "upload-called").exists() is after_upload - - -@pytest.mark.parametrize("existing", [False, True]) -@pytest.mark.parametrize("problem", ["missing", "duplicate", "wrong-name", "wrong-digest", "wrong-size", "starter", "no-digest"]) -def test_bad_durable_asset_stops_publication(tmp_path: Path, github_release: dict[str, str], existing: bool, problem: str) -> None: - metadata = json.loads((tmp_path / "uploaded-assets.json").read_text(encoding="utf-8"))[1][0] - assets = [metadata] - if problem == "missing": - assets = [] - elif problem == "duplicate": - assets.append(metadata) + assert f"Dispatch with --ref {BASELINE}" in result.stdout + assert not output.exists() + + +GH_FIXTURE = r"""import hashlib, json, os, sys +from pathlib import Path +args = sys.argv[1:] +root = Path(os.environ["RELEASE_FIXTURE"]) +tag = os.environ["RELEASE_TAG"] +asset = os.environ["RELEASE_ASSET"] +with (root / "api-calls").open("a", encoding="utf-8") as stream: + stream.write(json.dumps(args) + "\n") +metadata = {"id": 7, "name": asset, "size": len((root / asset).read_bytes()), + "digest": "sha256:" + hashlib.sha256((root / asset).read_bytes()).hexdigest(), "state": "uploaded"} +assets = [metadata] if (root / "uploaded").exists() else [] +release = {"id": 42, "url": "https://api.github.com/repos/owner/repo/releases/42", "tag_name": tag, + "name": tag, "draft": True, "prerelease": False, "immutable": False, "assets": assets} +if any(item.endswith("/releases?per_page=100") for item in args): + value = [[release]] +elif args[0] == "release": + value = {"databaseId": 42, "tagName": tag} +elif any("uploads.github.com" in item for item in args): + (root / "uploaded").touch() + value = {} +elif "PATCH" in args: + (root / "published").touch() + value = dict(release, draft=False, immutable=True) +elif args[1].endswith("/releases/assets/7"): + sys.stdout.buffer.write((root / asset).read_bytes()) + raise SystemExit(0) +elif "/git/ref/tags/" in args[1]: + value = {"ref": "refs/tags/" + tag, "object": {"type": "commit", "sha": os.environ.get("REMOTE_COMMIT", "a" * 40)}} +elif any("/assets?" in item for item in args): + value = [assets] +else: + value = release +print(json.dumps(value)) +""" + + +def test_installed_cli_binds_trusted_target_and_explicit_publication(tmp_path: Path) -> None: + """Exercise actual preflight and writer shell with the pinned installed CLI.""" + fixture = tmp_path / "provider with spaces" + fixture.mkdir() + program = fixture / "gh_fixture.py" + program.write_text(GH_FIXTURE, encoding="utf-8") + if os.name == "nt": + executable = fixture / "gh.cmd" + executable.write_text(f'@"{sys.executable}" "%~dp0gh_fixture.py" %*\r\n', encoding="utf-8", newline="") else: - field, value = { - "wrong-name": ("name", "wrong.tar.gz"), - "wrong-digest": ("digest", "sha256:" + "0" * 64), - "wrong-size": ("size", 999), - "starter": ("state", "starter"), - "no-digest": ("digest", None), - }[problem] - metadata[field] = value - write_json(tmp_path / "uploaded-assets.json", [assets]) - if existing: - write_json(tmp_path / "assets.json", [assets]) - result = run_shell(GITHUB_STUB + shell_step("Attach baseline and publish draft"), tmp_path, github_release) - assert result.returncode != 0 - assert not (tmp_path / "publish-called").exists() - assert (tmp_path / "upload-called").exists() is (not existing or problem in {"missing", "wrong-name"}) - - -def test_upload_failure_leaves_draft_unpublished(tmp_path: Path, github_release: dict[str, str]) -> None: - result = run_shell(GITHUB_STUB + shell_step("Attach baseline and publish draft"), tmp_path, {**github_release, "UPLOAD_STATUS": "8"}) - assert result.returncode == 8 - assert (tmp_path / "upload-called").exists() - assert not (tmp_path / "publish-called").exists() - - -def test_publication_failure_propagates_without_deleting_uploaded_asset(tmp_path: Path, github_release: dict[str, str]) -> None: - result = run_shell(GITHUB_STUB + shell_step("Attach baseline and publish draft"), tmp_path, {**github_release, "PUBLISH_STATUS": "9"}) - assert result.returncode != 0 - assert (tmp_path / "upload-called").exists() - assert (tmp_path / "publish-called").exists() - assert "DELETE" not in (tmp_path / "api-calls").read_text(encoding="utf-8") + executable = fixture / "gh" + executable.write_text(f'#!/bin/sh\nexec {shlex.quote(sys.executable)} {shlex.quote(str(program))} "$@"\n', encoding="utf-8") + executable.chmod(0o755) + asset = f"la-stack-{BASELINE}-criterion-baseline.tar.gz" + (fixture / asset).write_bytes(b"raw archive\r\n\xff") + output = fixture / "outputs" + env = { + "PATH": str(fixture) + os.pathsep + os.environ["PATH"], + "RELEASE_FIXTURE": str(fixture), + "RELEASE_TAG": BASELINE, + "RELEASE_ASSET": asset, + "GH_REPO": "owner/repo", + "GITHUB_REF": f"refs/tags/{BASELINE}", + "GITHUB_SHA": "a" * 40, + "GITHUB_OUTPUT": output.as_posix(), + "RUNNER_TEMP": fixture.as_posix(), + "GITHUB_WORKSPACE": fixture.as_posix(), + } + # Substitute only uvx's environment acquisition; retain its exact package + # arguments and execute the already locked package in a real child process. + runner = ( + 'uvx() { [[ "$1 $2 $3 $4 $5" == "--python 3.15 --from research-repo-tools==0.1.9 research-repo-tools" ]] || return 90; ' + f'shift 5; {shlex.quote(sys.executable)} -m research_repo_tools "$@"; }}\n' + ) + result = run_shell(runner + shell_step("Validate draft release target"), fixture, env) + assert result.returncode == 0, result.stderr + target = output.read_text(encoding="utf-8").strip().removeprefix("target=") + assert json.loads(target)["release_id"] == 42 + assert json.loads(target)["commit"] == "a" * 40 + assert json.loads(target)["expected_title"] == BASELINE + stale = run_shell(runner + shell_step("Attach baseline and publish draft"), fixture, {**env, "RELEASE_TARGET": target, "REMOTE_COMMIT": "b" * 40}) + assert stale.returncode != 0 + assert not (fixture / "published").exists() + result = run_shell(runner + shell_step("Attach baseline and publish draft"), fixture, {**env, "RELEASE_TARGET": target}) + assert result.returncode == 0, result.stderr + assert (fixture / "uploaded").exists() + assert (fixture / "published").exists() + calls = [json.loads(line) for line in (fixture / "api-calls").read_text(encoding="utf-8").splitlines()] + assert any("PATCH" in call for call in calls) + assert any("/releases/assets/7" in item for call in calls for item in call) diff --git a/scripts/tests/test_toolchain_integration.py b/scripts/tests/test_toolchain_integration.py index fc1ea04..cb08f4a 100644 --- a/scripts/tests/test_toolchain_integration.py +++ b/scripts/tests/test_toolchain_integration.py @@ -125,7 +125,7 @@ def fixture_wheel(registry: Path, requirement: str) -> None: distribution = name.replace("-", "_") info = f"{distribution}-{version}.dist-info" with zipfile.ZipFile(registry / f"{distribution}-{version}-py3-none-any.whl", "w") as archive: - archive.writestr(f"{info}/METADATA", f"Metadata-Version: 2.3\nName: {name}\nVersion: {version}\nRequires-Python: >=3.14\n") + archive.writestr(f"{info}/METADATA", f"Metadata-Version: 2.3\nName: {name}\nVersion: {version}\nRequires-Python: >=3.15\n") archive.writestr(f"{info}/WHEEL", "Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n") archive.writestr(f"{info}/RECORD", "") @@ -138,8 +138,7 @@ def test_python_update_preserves_shared_pin_and_tools_with_explicit_dev_sync(tmp registry.mkdir() original = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8") # Empty fixture wheels represent resolver candidates, not executable tools. - # Disable building the consumer's own scripts package in this disposable fixture. - source = original.replace("package = true", "package = false\ndefault-groups = []") + source = original.replace("package = false", "package = false\ndefault-groups = []") manifest = root / "pyproject.toml" manifest.write_text(source, encoding="utf-8") before = tomllib.loads(source) @@ -168,7 +167,7 @@ def test_python_update_preserves_shared_pin_and_tools_with_explicit_dev_sync(tmp run_command("uv", ["lock", "--upgrade"], cwd=root) lock = tomllib.loads((root / "uv.lock").read_text(encoding="utf-8")) packages = {package["name"]: package["version"] for package in lock["package"]} - assert packages["research-repo-tools"] == "0.1.8" + assert packages["research-repo-tools"] == "0.1.9" assert packages["ruff"] == "99.0.0" run_command("uv", ["sync", "--locked", "--group", "dev"], cwd=root) installed = run_command("uv", ["run", "--locked", "--no-sync", "python", "-c", "from importlib.metadata import version; print(version('ruff'))"], cwd=root) diff --git a/tooling/criterion-inventory.toml b/tooling/criterion-inventory.toml new file mode 100644 index 0000000..2a0a5dd --- /dev/null +++ b/tooling/criterion-inventory.toml @@ -0,0 +1,431 @@ +# Full unfiltered native suites; required IDs include every report and plot consumer. +schema = 1 +required = [ + "d16/faer_det_from_ldlt", + "d16/faer_det_from_lu", + "d16/faer_det_via_lu", + "d16/faer_dot_native", + "d16/faer_inf_norm", + "d16/faer_ldlt", + "d16/faer_ldlt_solve", + "d16/faer_lu", + "d16/faer_lu_solve", + "d16/faer_norm2_sq", + "d16/faer_solve_from_ldlt", + "d16/faer_solve_from_lu", + "d16/la_stack_det", + "d16/la_stack_det_from_ldlt", + "d16/la_stack_det_from_lu", + "d16/la_stack_det_via_lu", + "d16/la_stack_dot", + "d16/la_stack_inf_norm", + "d16/la_stack_ldlt", + "d16/la_stack_ldlt_solve", + "d16/la_stack_lu", + "d16/la_stack_lu_solve", + "d16/la_stack_norm2_sq", + "d16/la_stack_solve_from_ldlt", + "d16/la_stack_solve_from_lu", + "d16/nalgebra_cholesky", + "d16/nalgebra_cholesky_solve", + "d16/nalgebra_det_from_cholesky", + "d16/nalgebra_det_from_lu", + "d16/nalgebra_det_via_lu", + "d16/nalgebra_dot", + "d16/nalgebra_inf_norm", + "d16/nalgebra_lu", + "d16/nalgebra_lu_solve", + "d16/nalgebra_norm_squared", + "d16/nalgebra_solve_from_cholesky", + "d16/nalgebra_solve_from_lu", + "d2/faer_det_from_ldlt", + "d2/faer_det_from_lu", + "d2/faer_det_via_lu", + "d2/faer_dot_native", + "d2/faer_inf_norm", + "d2/faer_ldlt", + "d2/faer_ldlt_solve", + "d2/faer_lu", + "d2/faer_lu_solve", + "d2/faer_norm2_sq", + "d2/faer_solve_from_ldlt", + "d2/faer_solve_from_lu", + "d2/la_stack_det", + "d2/la_stack_det_from_ldlt", + "d2/la_stack_det_from_lu", + "d2/la_stack_det_via_lu", + "d2/la_stack_dot", + "d2/la_stack_inf_norm", + "d2/la_stack_ldlt", + "d2/la_stack_ldlt_solve", + "d2/la_stack_lu", + "d2/la_stack_lu_solve", + "d2/la_stack_norm2_sq", + "d2/la_stack_solve_from_ldlt", + "d2/la_stack_solve_from_lu", + "d2/nalgebra_cholesky", + "d2/nalgebra_cholesky_solve", + "d2/nalgebra_det_from_cholesky", + "d2/nalgebra_det_from_lu", + "d2/nalgebra_det_via_lu", + "d2/nalgebra_dot", + "d2/nalgebra_inf_norm", + "d2/nalgebra_lu", + "d2/nalgebra_lu_solve", + "d2/nalgebra_norm_squared", + "d2/nalgebra_solve_from_cholesky", + "d2/nalgebra_solve_from_lu", + "d3/faer_det_from_ldlt", + "d3/faer_det_from_lu", + "d3/faer_det_via_lu", + "d3/faer_dot_native", + "d3/faer_inf_norm", + "d3/faer_ldlt", + "d3/faer_ldlt_solve", + "d3/faer_lu", + "d3/faer_lu_solve", + "d3/faer_norm2_sq", + "d3/faer_solve_from_ldlt", + "d3/faer_solve_from_lu", + "d3/la_stack_det", + "d3/la_stack_det_from_ldlt", + "d3/la_stack_det_from_lu", + "d3/la_stack_det_via_lu", + "d3/la_stack_dot", + "d3/la_stack_inf_norm", + "d3/la_stack_ldlt", + "d3/la_stack_ldlt_solve", + "d3/la_stack_lu", + "d3/la_stack_lu_solve", + "d3/la_stack_norm2_sq", + "d3/la_stack_solve_from_ldlt", + "d3/la_stack_solve_from_lu", + "d3/nalgebra_cholesky", + "d3/nalgebra_cholesky_solve", + "d3/nalgebra_det_from_cholesky", + "d3/nalgebra_det_from_lu", + "d3/nalgebra_det_via_lu", + "d3/nalgebra_dot", + "d3/nalgebra_inf_norm", + "d3/nalgebra_lu", + "d3/nalgebra_lu_solve", + "d3/nalgebra_norm_squared", + "d3/nalgebra_solve_from_cholesky", + "d3/nalgebra_solve_from_lu", + "d32/faer_det_from_ldlt", + "d32/faer_det_from_lu", + "d32/faer_det_via_lu", + "d32/faer_dot_native", + "d32/faer_inf_norm", + "d32/faer_ldlt", + "d32/faer_ldlt_solve", + "d32/faer_lu", + "d32/faer_lu_solve", + "d32/faer_norm2_sq", + "d32/faer_solve_from_ldlt", + "d32/faer_solve_from_lu", + "d32/la_stack_det", + "d32/la_stack_det_from_ldlt", + "d32/la_stack_det_from_lu", + "d32/la_stack_det_via_lu", + "d32/la_stack_dot", + "d32/la_stack_inf_norm", + "d32/la_stack_ldlt", + "d32/la_stack_ldlt_solve", + "d32/la_stack_lu", + "d32/la_stack_lu_solve", + "d32/la_stack_norm2_sq", + "d32/la_stack_solve_from_ldlt", + "d32/la_stack_solve_from_lu", + "d32/nalgebra_cholesky", + "d32/nalgebra_cholesky_solve", + "d32/nalgebra_det_from_cholesky", + "d32/nalgebra_det_from_lu", + "d32/nalgebra_det_via_lu", + "d32/nalgebra_dot", + "d32/nalgebra_inf_norm", + "d32/nalgebra_lu", + "d32/nalgebra_lu_solve", + "d32/nalgebra_norm_squared", + "d32/nalgebra_solve_from_cholesky", + "d32/nalgebra_solve_from_lu", + "d4/faer_det_from_ldlt", + "d4/faer_det_from_lu", + "d4/faer_det_via_lu", + "d4/faer_dot_native", + "d4/faer_inf_norm", + "d4/faer_ldlt", + "d4/faer_ldlt_solve", + "d4/faer_lu", + "d4/faer_lu_solve", + "d4/faer_norm2_sq", + "d4/faer_solve_from_ldlt", + "d4/faer_solve_from_lu", + "d4/la_stack_det", + "d4/la_stack_det_from_ldlt", + "d4/la_stack_det_from_lu", + "d4/la_stack_det_via_lu", + "d4/la_stack_dot", + "d4/la_stack_inf_norm", + "d4/la_stack_ldlt", + "d4/la_stack_ldlt_solve", + "d4/la_stack_lu", + "d4/la_stack_lu_solve", + "d4/la_stack_norm2_sq", + "d4/la_stack_solve_from_ldlt", + "d4/la_stack_solve_from_lu", + "d4/nalgebra_cholesky", + "d4/nalgebra_cholesky_solve", + "d4/nalgebra_det_from_cholesky", + "d4/nalgebra_det_from_lu", + "d4/nalgebra_det_via_lu", + "d4/nalgebra_dot", + "d4/nalgebra_inf_norm", + "d4/nalgebra_lu", + "d4/nalgebra_lu_solve", + "d4/nalgebra_norm_squared", + "d4/nalgebra_solve_from_cholesky", + "d4/nalgebra_solve_from_lu", + "d5/faer_det_from_ldlt", + "d5/faer_det_from_lu", + "d5/faer_det_via_lu", + "d5/faer_dot_native", + "d5/faer_inf_norm", + "d5/faer_ldlt", + "d5/faer_ldlt_solve", + "d5/faer_lu", + "d5/faer_lu_solve", + "d5/faer_norm2_sq", + "d5/faer_solve_from_ldlt", + "d5/faer_solve_from_lu", + "d5/la_stack_det", + "d5/la_stack_det_from_ldlt", + "d5/la_stack_det_from_lu", + "d5/la_stack_det_via_lu", + "d5/la_stack_dot", + "d5/la_stack_inf_norm", + "d5/la_stack_ldlt", + "d5/la_stack_ldlt_solve", + "d5/la_stack_lu", + "d5/la_stack_lu_solve", + "d5/la_stack_norm2_sq", + "d5/la_stack_solve_from_ldlt", + "d5/la_stack_solve_from_lu", + "d5/nalgebra_cholesky", + "d5/nalgebra_cholesky_solve", + "d5/nalgebra_det_from_cholesky", + "d5/nalgebra_det_from_lu", + "d5/nalgebra_det_via_lu", + "d5/nalgebra_dot", + "d5/nalgebra_inf_norm", + "d5/nalgebra_lu", + "d5/nalgebra_lu_solve", + "d5/nalgebra_norm_squared", + "d5/nalgebra_solve_from_cholesky", + "d5/nalgebra_solve_from_lu", + "d64/faer_det_from_ldlt", + "d64/faer_det_from_lu", + "d64/faer_det_via_lu", + "d64/faer_dot_native", + "d64/faer_inf_norm", + "d64/faer_ldlt", + "d64/faer_ldlt_solve", + "d64/faer_lu", + "d64/faer_lu_solve", + "d64/faer_norm2_sq", + "d64/faer_solve_from_ldlt", + "d64/faer_solve_from_lu", + "d64/la_stack_det", + "d64/la_stack_det_from_ldlt", + "d64/la_stack_det_from_lu", + "d64/la_stack_det_via_lu", + "d64/la_stack_dot", + "d64/la_stack_inf_norm", + "d64/la_stack_ldlt", + "d64/la_stack_ldlt_solve", + "d64/la_stack_lu", + "d64/la_stack_lu_solve", + "d64/la_stack_norm2_sq", + "d64/la_stack_solve_from_ldlt", + "d64/la_stack_solve_from_lu", + "d64/nalgebra_cholesky", + "d64/nalgebra_cholesky_solve", + "d64/nalgebra_det_from_cholesky", + "d64/nalgebra_det_from_lu", + "d64/nalgebra_det_via_lu", + "d64/nalgebra_dot", + "d64/nalgebra_inf_norm", + "d64/nalgebra_lu", + "d64/nalgebra_lu_solve", + "d64/nalgebra_norm_squared", + "d64/nalgebra_solve_from_cholesky", + "d64/nalgebra_solve_from_lu", + "d8/faer_det_from_ldlt", + "d8/faer_det_from_lu", + "d8/faer_det_via_lu", + "d8/faer_dot_native", + "d8/faer_inf_norm", + "d8/faer_ldlt", + "d8/faer_ldlt_solve", + "d8/faer_lu", + "d8/faer_lu_solve", + "d8/faer_norm2_sq", + "d8/faer_solve_from_ldlt", + "d8/faer_solve_from_lu", + "d8/la_stack_det", + "d8/la_stack_det_from_ldlt", + "d8/la_stack_det_from_ldlt_balanced_range", + "d8/la_stack_det_from_lu", + "d8/la_stack_det_from_lu_balanced_range", + "d8/la_stack_det_via_lu", + "d8/la_stack_dot", + "d8/la_stack_inf_norm", + "d8/la_stack_ldlt", + "d8/la_stack_ldlt_ill_conditioned", + "d8/la_stack_ldlt_solve", + "d8/la_stack_lu", + "d8/la_stack_lu_ill_conditioned", + "d8/la_stack_lu_pivoting", + "d8/la_stack_lu_solve", + "d8/la_stack_norm2_sq", + "d8/la_stack_solve_from_ldlt", + "d8/la_stack_solve_from_lu", + "d8/nalgebra_cholesky", + "d8/nalgebra_cholesky_solve", + "d8/nalgebra_det_from_cholesky", + "d8/nalgebra_det_from_lu", + "d8/nalgebra_det_via_lu", + "d8/nalgebra_dot", + "d8/nalgebra_inf_norm", + "d8/nalgebra_lu", + "d8/nalgebra_lu_solve", + "d8/nalgebra_norm_squared", + "d8/nalgebra_solve_from_cholesky", + "d8/nalgebra_solve_from_lu", + "exact_d2/det", + "exact_d2/det_direct", + "exact_d2/det_direct_with_errbound", + "exact_d2/det_errbound", + "exact_d2/det_exact", + "exact_d2/det_exact_f64_result", + "exact_d2/det_exact_rounded_f64", + "exact_d2/det_sign_exact", + "exact_d2/solve_exact", + "exact_d2/solve_exact_f64_result", + "exact_d2/solve_exact_rounded_f64", + "exact_d3/det", + "exact_d3/det_direct", + "exact_d3/det_direct_with_errbound", + "exact_d3/det_errbound", + "exact_d3/det_exact", + "exact_d3/det_exact_f64_result", + "exact_d3/det_exact_rounded_f64", + "exact_d3/det_sign_exact", + "exact_d3/solve_exact", + "exact_d3/solve_exact_f64_result", + "exact_d3/solve_exact_rounded_f64", + "exact_d4/det", + "exact_d4/det_direct", + "exact_d4/det_direct_with_errbound", + "exact_d4/det_errbound", + "exact_d4/det_exact", + "exact_d4/det_exact_f64_result", + "exact_d4/det_exact_rounded_f64", + "exact_d4/det_sign_exact", + "exact_d4/solve_exact", + "exact_d4/solve_exact_f64_result", + "exact_d4/solve_exact_rounded_f64", + "exact_d5/det", + "exact_d5/det_exact", + "exact_d5/det_exact_f64_result", + "exact_d5/det_exact_rounded_f64", + "exact_d5/det_sign_exact", + "exact_d5/solve_exact", + "exact_d5/solve_exact_f64_result", + "exact_d5/solve_exact_rounded_f64", + "exact_hilbert_4x4/det_exact", + "exact_hilbert_4x4/det_sign_exact", + "exact_hilbert_4x4/solve_exact", + "exact_hilbert_4x4/solve_exact_f64_result", + "exact_hilbert_4x4/solve_exact_rounded_f64", + "exact_hilbert_5x5/det_exact", + "exact_hilbert_5x5/det_sign_exact", + "exact_hilbert_5x5/solve_exact", + "exact_hilbert_5x5/solve_exact_f64_result", + "exact_hilbert_5x5/solve_exact_rounded_f64", + "exact_large_entries_3x3/det_exact", + "exact_large_entries_3x3/det_sign_exact", + "exact_large_entries_3x3/solve_exact", + "exact_large_entries_3x3/solve_exact_f64_result", + "exact_large_entries_3x3/solve_exact_rounded_f64", + "exact_near_singular_3x3/det_exact", + "exact_near_singular_3x3/det_sign_exact", + "exact_near_singular_3x3/solve_exact", + "exact_near_singular_3x3/solve_exact_f64_result", + "exact_near_singular_3x3/solve_exact_rounded_f64", + "exact_random_corpus_d2/det_exact", + "exact_random_corpus_d2/det_sign_exact", + "exact_random_corpus_d2/solve_exact", + "exact_random_corpus_d2/solve_exact_f64_result", + "exact_random_corpus_d2/solve_exact_rounded_f64", + "exact_random_corpus_d3/det_exact", + "exact_random_corpus_d3/det_sign_exact", + "exact_random_corpus_d3/solve_exact", + "exact_random_corpus_d3/solve_exact_f64_result", + "exact_random_corpus_d3/solve_exact_rounded_f64", + "exact_random_corpus_d4/det_exact", + "exact_random_corpus_d4/det_sign_exact", + "exact_random_corpus_d4/solve_exact", + "exact_random_corpus_d4/solve_exact_f64_result", + "exact_random_corpus_d4/solve_exact_rounded_f64", + "exact_random_corpus_d5/det_exact", + "exact_random_corpus_d5/det_sign_exact", + "exact_random_corpus_d5/solve_exact", + "exact_random_corpus_d5/solve_exact_f64_result", + "exact_random_corpus_d5/solve_exact_rounded_f64", + "rational_input_d2/det_big_rational_gaussian", + "rational_input_d2/det_row_cleared_bareiss", + "rational_input_d2/det_sign_row_cleared_bareiss", + "rational_input_d2/solve_big_rational_gaussian", + "rational_input_d2/solve_row_cleared_bareiss", + "rational_input_d3/det_big_rational_gaussian", + "rational_input_d3/det_row_cleared_bareiss", + "rational_input_d3/det_sign_row_cleared_bareiss", + "rational_input_d3/solve_big_rational_gaussian", + "rational_input_d3/solve_row_cleared_bareiss", + "rational_input_d4/det_big_rational_gaussian", + "rational_input_d4/det_row_cleared_bareiss", + "rational_input_d4/det_sign_row_cleared_bareiss", + "rational_input_d4/solve_big_rational_gaussian", + "rational_input_d4/solve_row_cleared_bareiss", + "rational_input_d5/det_big_rational_gaussian", + "rational_input_d5/det_row_cleared_bareiss", + "rational_input_d5/det_sign_row_cleared_bareiss", + "rational_input_d5/solve_big_rational_gaussian", + "rational_input_d5/solve_row_cleared_bareiss", + "rational_input_d6/det_big_rational_gaussian", + "rational_input_d6/det_row_cleared_bareiss", + "rational_input_d6/det_sign_row_cleared_bareiss", + "rational_input_d6/solve_big_rational_gaussian", + "rational_input_d6/solve_row_cleared_bareiss", + "rational_input_d7/det_big_rational_gaussian", + "rational_input_d7/det_row_cleared_bareiss", + "rational_input_d7/det_sign_row_cleared_bareiss", + "rational_input_d7/solve_big_rational_gaussian", + "rational_input_d7/solve_row_cleared_bareiss", + "rational_input_d8/det_big_rational_gaussian", + "rational_input_d8/det_row_cleared_bareiss", + "rational_input_d8/det_sign_row_cleared_bareiss", + "rational_input_d8/solve_big_rational_gaussian", + "rational_input_d8/solve_row_cleared_bareiss", +] + +[[suites]] +name = "vs_linalg" +command = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "baseline-vs_linalg", "list" ] +timeout = 1500 + +[[suites]] +name = "exact" +command = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "baseline-exact", "list" ] +timeout = 1500 diff --git a/tooling/criterion-saved.toml b/tooling/criterion-saved.toml new file mode 100644 index 0000000..e75ad5d --- /dev/null +++ b/tooling/criterion-saved.toml @@ -0,0 +1,9 @@ +# The release adapter supplies the workflow tag; all verification is shared. +schema = 1 +saved-sample = "last" +inventory = "target/release-benchmark-inventory.json" +raw-files = [ "benchmark.json", "estimates.json", "sample.json", "tukey.json" ] +sample-count = 100 +statistics = [ "mean", "median" ] +confidence-level = 0.95 +unit = "ns" diff --git a/tooling/performance-legacy.toml b/tooling/performance-legacy.toml new file mode 100644 index 0000000..5768242 --- /dev/null +++ b/tooling/performance-legacy.toml @@ -0,0 +1,80 @@ +# Read the retained performance.csv format; never rewrite originals or complete runs. +schema = 1 +schema-field = "schema_version" +schemas = [ 1 ] +hash-field = "csv.sha256" +benchmark = "benchmark_id" +statistic = "median" +unit = "ns" +header = [ + "schema_version", + "suite", + "scope", + "benchmark_id", + "group", + "benchmark", + "baseline_benchmark", + "coverage_status", + "coverage_note", + "baseline_median_ns", + "baseline_ci_lower_ns", + "baseline_ci_upper_ns", + "current_median_ns", + "current_ci_lower_ns", + "current_ci_upper_ns", + "baseline_nalgebra_median_ns", + "baseline_nalgebra_ci_lower_ns", + "baseline_nalgebra_ci_upper_ns", + "baseline_faer_median_ns", + "baseline_faer_ci_lower_ns", + "baseline_faer_ci_upper_ns", +] +equal = [ + [ "release.baseline", "benchmark_provenance.baseline" ], + [ "release.current", "benchmark_provenance.current" ], + [ "benchmark_provenance.measurement.baseline_commit", "benchmark_provenance.validation.baseline_commit" ], + [ "benchmark_provenance.measurement.current_commit", "benchmark_provenance.validation.current_commit" ], + [ "benchmark_provenance.measurement.current_commit", "benchmark_provenance.publication.commit" ], + [ "benchmark_provenance.measurement.harness_sha256", "benchmark_provenance.publication.harness_sha256" ], + [ "benchmark_provenance.measurement.cargo_lock_sha256", "benchmark_provenance.publication.cargo_lock_sha256" ], +] + +[constants] +schema_version = "1" +scope = "release-signal" + +[assertions] +"benchmark_provenance.schema" = 2 +"benchmark_provenance.mode" = "shared-current-harness" +"benchmark_provenance.criterion.statistic" = "median" +"benchmark_provenance.criterion.scope" = "release-signal" +"benchmark_provenance.validation.baseline_revision" = "passed" +"benchmark_provenance.validation.current_revision" = "passed" +"benchmark_provenance.validation.harness" = "shared-current" +"benchmark_provenance.publication.correctness_gate" = "passed" + +[baseline] +point = "baseline_median_ns" +lower = "baseline_ci_lower_ns" +upper = "baseline_ci_upper_ns" + +[current] +point = "current_median_ns" +lower = "current_ci_lower_ns" +upper = "current_ci_upper_ns" + +[coverage] +column = "coverage_status" +common = "comparable" +added = "current-only" +missing = "baseline-only" + +[sources.baseline] +revision = "benchmark_provenance.measurement.baseline_commit" +release = "release.baseline" +record = "benchmark_provenance" + +[sources.current] +revision = "benchmark_provenance.measurement.current_commit" +release = "release.current" +record = "benchmark_provenance" diff --git a/tooling/performance-plot.toml b/tooling/performance-plot.toml new file mode 100644 index 0000000..1c87651 --- /dev/null +++ b/tooling/performance-plot.toml @@ -0,0 +1,13 @@ +schema = 1 +dimensions = [ 2, 3, 4, 5, 8, 16, 32, 64 ] +unit = "ns" +series = [ + { name = "la-stack current", label = "la-stack" }, + { name = "nalgebra", label = "nalgebra" }, + { name = "faer", label = "faer" }, +] + +[plot] +x-label = "Dimension D" +y-label = "Time" +x-scale = "linear" diff --git a/tooling/performance-policy.toml b/tooling/performance-policy.toml new file mode 100644 index 0000000..1572363 --- /dev/null +++ b/tooling/performance-policy.toml @@ -0,0 +1,80 @@ +# la-stack scientific selections; generic execution and rendering are shared. +schema = 1 +dimensions = [ 2, 3, 4, 5, 8, 16, 32, 64 ] +minimum-release = [ 0, 4, 4 ] +modern-api-release = [ 0, 4, 6 ] +legacy-api-cfg = "la_stack_pre_rational_input_api" +library-benchmarks = [ "la_stack_det", "la_stack_det_from_ldlt", "la_stack_det_from_lu", "la_stack_det_via_lu", "la_stack_dot", "la_stack_inf_norm", "la_stack_ldlt", "la_stack_ldlt_solve", "la_stack_lu", "la_stack_lu_solve", "la_stack_norm2_sq", "la_stack_solve_from_ldlt", "la_stack_solve_from_lu" ] +unavailable-prefixes = [ "rational_input_", "canonical_conversion_", "det4_diagnostic_" ] +unavailable-benchmarks = [ "la_stack_norm2" ] +unavailable-benchmark-prefixes = [ "la_stack_norm2_scenario_", "iterative_f64_hypot_norm2_scenario_", "delaunay_scaled_norm_norm2_scenario_" ] + +[exact] +exact_d2 = [ "det", "det_direct", "det_direct_with_errbound", "det_errbound", "det_exact", "det_exact_f64_result", "det_exact_rounded_f64", "det_sign_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_d3 = [ "det", "det_direct", "det_direct_with_errbound", "det_errbound", "det_exact", "det_exact_f64_result", "det_exact_rounded_f64", "det_sign_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_d4 = [ "det", "det_direct", "det_direct_with_errbound", "det_errbound", "det_exact", "det_exact_f64_result", "det_exact_rounded_f64", "det_sign_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_d5 = [ "det", "det_exact", "det_exact_f64_result", "det_exact_rounded_f64", "det_sign_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_random_corpus_d2 = [ "det_sign_exact", "det_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_random_corpus_d3 = [ "det_sign_exact", "det_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_random_corpus_d4 = [ "det_sign_exact", "det_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_random_corpus_d5 = [ "det_sign_exact", "det_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +rational_input_d2 = [ "det_sign_row_cleared_bareiss", "det_row_cleared_bareiss", "det_big_rational_gaussian", "solve_row_cleared_bareiss", "solve_big_rational_gaussian" ] +rational_input_d3 = [ "det_sign_row_cleared_bareiss", "det_row_cleared_bareiss", "det_big_rational_gaussian", "solve_row_cleared_bareiss", "solve_big_rational_gaussian" ] +rational_input_d4 = [ "det_sign_row_cleared_bareiss", "det_row_cleared_bareiss", "det_big_rational_gaussian", "solve_row_cleared_bareiss", "solve_big_rational_gaussian" ] +rational_input_d5 = [ "det_sign_row_cleared_bareiss", "det_row_cleared_bareiss", "det_big_rational_gaussian", "solve_row_cleared_bareiss", "solve_big_rational_gaussian" ] +rational_input_d6 = [ "det_sign_row_cleared_bareiss", "det_row_cleared_bareiss", "det_big_rational_gaussian", "solve_row_cleared_bareiss", "solve_big_rational_gaussian" ] +rational_input_d7 = [ "det_sign_row_cleared_bareiss", "det_row_cleared_bareiss", "det_big_rational_gaussian", "solve_row_cleared_bareiss", "solve_big_rational_gaussian" ] +rational_input_d8 = [ "det_sign_row_cleared_bareiss", "det_row_cleared_bareiss", "det_big_rational_gaussian", "solve_row_cleared_bareiss", "solve_big_rational_gaussian" ] +exact_near_singular_3x3 = [ "det_sign_exact", "det_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_large_entries_3x3 = [ "det_sign_exact", "det_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_hilbert_4x4 = [ "det_sign_exact", "det_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] +exact_hilbert_5x5 = [ "det_sign_exact", "det_exact", "solve_exact", "solve_exact_f64_result", "solve_exact_rounded_f64" ] + +[additional] +"8" = [ "la_stack_lu_pivoting", "la_stack_lu_ill_conditioned", "la_stack_ldlt_ill_conditioned", "la_stack_det_from_lu_balanced_range", "la_stack_det_from_ldlt_balanced_range" ] + +[peers] +la_stack_det_via_lu = [ "nalgebra_det_via_lu", "faer_det_via_lu" ] +la_stack_lu = [ "nalgebra_lu", "faer_lu" ] +la_stack_lu_solve = [ "nalgebra_lu_solve", "faer_lu_solve" ] +la_stack_solve_from_lu = [ "nalgebra_solve_from_lu", "faer_solve_from_lu" ] +la_stack_det_from_lu = [ "nalgebra_det_from_lu", "faer_det_from_lu" ] +la_stack_dot = [ "nalgebra_dot", "faer_dot_native" ] +la_stack_norm2_sq = [ "nalgebra_norm_squared", "faer_norm2_sq" ] +la_stack_inf_norm = [ "nalgebra_inf_norm", "faer_inf_norm" ] +la_stack_ldlt = [ "nalgebra_cholesky", "faer_ldlt" ] +la_stack_ldlt_solve = [ "nalgebra_cholesky_solve", "faer_ldlt_solve" ] +la_stack_solve_from_ldlt = [ "nalgebra_solve_from_cholesky", "faer_solve_from_ldlt" ] +la_stack_det_from_ldlt = [ "nalgebra_det_from_cholesky", "faer_det_from_ldlt" ] + +[metrics.det_via_lu] +title = "Determinant via LU (factor + det)" +benchmarks = [ "la_stack_det_via_lu", "nalgebra_det_via_lu", "faer_det_via_lu" ] + +[metrics.lu] +title = "LU factorization" +benchmarks = [ "la_stack_lu", "nalgebra_lu", "faer_lu" ] + +[metrics.lu_solve] +title = "LU solve (factor + solve)" +benchmarks = [ "la_stack_lu_solve", "nalgebra_lu_solve", "faer_lu_solve" ] + +[metrics.solve_from_lu] +title = "Solve from precomputed LU" +benchmarks = [ "la_stack_solve_from_lu", "nalgebra_solve_from_lu", "faer_solve_from_lu" ] + +[metrics.det_from_lu] +title = "Determinant from precomputed LU" +benchmarks = [ "la_stack_det_from_lu", "nalgebra_det_from_lu", "faer_det_from_lu" ] + +[metrics.dot] +title = "Vector dot product" +benchmarks = [ "la_stack_dot", "nalgebra_dot", "faer_dot_native" ] + +[metrics.norm2_sq] +title = "Vector squared 2-norm" +benchmarks = [ "la_stack_norm2_sq", "nalgebra_norm_squared", "faer_norm2_sq" ] + +[metrics.inf_norm] +title = "Matrix infinity norm (max abs row sum)" +benchmarks = [ "la_stack_inf_norm", "nalgebra_inf_norm", "faer_inf_norm" ] diff --git a/tooling/performance.toml b/tooling/performance.toml index d498b39..8dac4cb 100644 --- a/tooling/performance.toml +++ b/tooling/performance.toml @@ -1,4 +1,4 @@ -# Shared measurement inputs; scientific row selection stays in performance_runs. +# Shared measurement inputs; scientific rows are declared in performance-policy.toml. schema = 1 command = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "current-all" ] sources = [ "src/**/*.rs", "Cargo.toml" ] @@ -11,7 +11,9 @@ harness = [ "rust-toolchain.toml", "tests/exact_bench_config.rs", "tooling/performance.toml", + "tooling/performance-policy.toml", "tooling/performance.just", + "tooling/criterion-inventory.toml", ] timeout = 7200 compatible = [ diff --git a/tooling/semgrep/.python-version b/tooling/semgrep/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/tooling/semgrep/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/tooling/semgrep/pyproject.toml b/tooling/semgrep/pyproject.toml new file mode 100644 index 0000000..01e51e8 --- /dev/null +++ b/tooling/semgrep/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "la-stack-semgrep" +version = "0.0.0" +requires-python = ">=3.14,<3.15" +dependencies = [ "semgrep==1.180.0" ] + +[tool.uv] +package = false +# Semgrep's sdist does not bundle its required native engine. +no-build-package = [ "semgrep" ] diff --git a/tooling/semgrep/uv.lock b/tooling/semgrep/uv.lock new file mode 100644 index 0000000..c716ebf --- /dev/null +++ b/tooling/semgrep/uv.lock @@ -0,0 +1,1055 @@ +version = 1 +revision = 5 +requires-python = "==3.14.*" + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "boltons" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/1f/6c0608d86e0fc77c982a2923ece80eef85f091f2332fc13cbce41d70d502/boltons-21.0.0.tar.gz", hash = "sha256:65e70a79a731a7fe6e98592ecfb5ccf2115873d01dbc576079874629e5c90f13", size = 180201, upload-time = "2021-05-17T01:20:17.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/a7/1a31561d10a089fcb46fe286766dd4e053a12f6e23b4fd1c26478aff2475/boltons-21.0.0-py2.py3-none-any.whl", hash = "sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b", size = 193723, upload-time = "2021-05-17T01:20:20.023Z" }, +] + +[[package]] +name = "bracex" +version = "3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/60/e2a6409041cb3f8ce7ac3d6917b5b30c39f81a0a5b2d414ec30f36fd96bf/bracex-3.1.tar.gz", hash = "sha256:35c69fc105f08d77c02722e8ae707c656ff298dd4d26c89bcb3c98fca8bc3614", size = 29040, upload-time = "2026-10-11T15:26:07.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/79/c1ba46e1acd235affd36ce1d3346320ddcda663c9482a9e5f34727db1d11/bracex-3.1-py3-none-any.whl", hash = "sha256:52002b8455658c21f3b6e5eb0f662b57ec9085f42709e58bc86eeab5bc4c37d7", size = 11925, upload-time = "2026-10-11T15:26:05.683Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" }, + { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" }, + { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" }, + { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" }, + { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" }, + { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "click-option-group" +version = "0.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" }, +] + +[[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 = "cryptography" +version = "50.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, +] + +[[package]] +name = "face" +version = "26.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boltons" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/fd/f84f0600bd72953d5a322f0dedbd4f900e2cedab718e6b6a093ae2d16aae/face-26.0.1.tar.gz", hash = "sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6", size = 51754, upload-time = "2026-06-17T23:14:39.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/24/0159c48d19c8b05e6969ee809bbbecc5a5c863f7e38c9327e2c63cb06f0f/face-26.0.1-py3-none-any.whl", hash = "sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d", size = 57572, upload-time = "2026-06-17T23:14:38.711Z" }, +] + +[[package]] +name = "glom" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "boltons" }, + { name = "face" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/74/8387f95565ba7c30cd152a585b275ebb9a834d1d32782425c5d2fe0a102c/glom-25.12.0.tar.gz", hash = "sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069", size = 196128, upload-time = "2025-12-29T06:29:07.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/2b/6ce81972d5c8cab9705fddce3153be63222d9e12fd96f8baba5038a744dd/googleapis_common_protos-1.75.5.tar.gz", hash = "sha256:c7a866fc34ed29a3b10af627a4b9b1dc2433313ca6e959f0ae4feb132047ed72", size = 156513, upload-time = "2026-09-29T19:26:14.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/b9/6b29500a1c581ff4d77fd83c6568d068bee06f1b139fb6eb0a4f2d4bce8a/googleapis_common_protos-1.75.5-py3-none-any.whl", hash = "sha256:d7285525c23039db98f2463e6d5a4f9b958b94d497f03a844ece3259c4e72d5d", size = 307737, upload-time = "2026-09-29T19:25:48.735Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "la-stack-semgrep" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "semgrep" }, +] + +[package.metadata] +requires-dist = [{ name = "semgrep", specifier = "==1.180.0" }] + +[[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 = "mcp" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, +] + +[[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 = "opentelemetry-api" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/36/7c307d9be8ce4ee7beb86d7f1d31027f2a6a89228240405a858d6e4d64f9/opentelemetry_instrumentation-0.58b0.tar.gz", hash = "sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705", size = 31549, upload-time = "2025-09-11T11:42:14.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019, upload-time = "2025-09-11T11:41:00.624Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/42/83ee32de763b919779aaa595b60c5a7b9c0a4b33952bbe432c5f6a783085/opentelemetry_instrumentation_requests-0.58b0.tar.gz", hash = "sha256:ae9495e6ff64e27bdb839fce91dbb4be56e325139828e8005f875baf41951a2e", size = 15188, upload-time = "2025-09-11T11:42:51.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/4d/f3476b28ea167d1762134352d01ae9693940a42c78994d9f1b32a4477816/opentelemetry_instrumentation_requests-0.58b0-py3-none-any.whl", hash = "sha256:672a0be0bb5b52bea0c11820b35e27edcf4cd22d34abe4afc59a92a80519f8a8", size = 12966, upload-time = "2025-09-11T11:41:52.67Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/a9/3888cb0470e6eb48ea17b6802275ae71df411edd6382b9a8e8f391936fda/opentelemetry_instrumentation_threading-0.58b0.tar.gz", hash = "sha256:f68c61f77841f9ff6270176f4d496c10addbceacd782af434d705f83e4504862", size = 8770, upload-time = "2025-09-11T11:42:56.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/add1076cb37980e617723a96e29c84006983e8ad6fc589dde7f69ddc57d4/opentelemetry_instrumentation_threading-0.58b0-py3-none-any.whl", hash = "sha256:eacc072881006aceb5b9b6831bcdce718c67ef6f31ac0b32bd6a23a94d979b4a", size = 9312, upload-time = "2025-09-11T11:41:58.603Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411, upload-time = "2025-09-11T11:43:05.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "peewee" +version = "3.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/b0/79462b42e89764998756e0557f2b58a15610a5b4512fbbcccae58fba7237/peewee-3.19.0.tar.gz", hash = "sha256:f88292a6f0d7b906cb26bca9c8599b8f4d8920ebd36124400d0cbaaaf915511f", size = 974035, upload-time = "2026-01-07T17:24:59.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/41/19c65578ef9a54b3083253c68a607f099642747168fe00f3a2bceb7c3a34/peewee-3.19.0-py3-none-any.whl", hash = "sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417", size = 411885, upload-time = "2026-01-07T17:24:58.33Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pycparser" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" }, +] + +[[package]] +name = "pydantic" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.50.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, + { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, + { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, + { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/3b/a5d2294799b53b448319978cfb5bd139d5a9d45e862af91661614f14c922/pydantic_settings-2.16.0.tar.gz", hash = "sha256:5b6c578049ede4db0e2ef3b4eaa4ad4069cfa9211f83fb38df899dfade50a614", size = 301031, upload-time = "2026-10-14T12:44:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/f4/b987bf8c51e5b19a95fa66d1ee596074141e085d9c2ddf97920803c7029b/pydantic_settings-2.16.0-py3-none-any.whl", hash = "sha256:7e73acf7f61936a15e5a3b6eedaea29f133357faf7272f2607ba479b049dd7f2", size = 80364, upload-time = "2026-10-14T12:44:08.233Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252, upload-time = "2026-09-28T18:40:42.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[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 = "rpds-py" +version = "2026.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/f2/67b94be1532767803415c1c5a1fd88ea487643d74a673cec1ba140af77bb/rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", size = 347178, upload-time = "2026-10-04T16:30:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/04/37/b751de2b59b0197a1d92a5dd491de88e8a5e928c2e6562581974f1e85263/rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", size = 341878, upload-time = "2026-10-04T16:30:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/72/e2/5873bc4643c250db9e05d48dc0c93763d4aa68bc3b81164cb1af3b45b284/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", size = 373963, upload-time = "2026-10-04T16:30:04.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/03/5acf7632158247f3f6386ff0af3a1ee48167d575037e8d0920594b76d92b/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", size = 377975, upload-time = "2026-10-04T16:30:05.555Z" }, + { url = "https://files.pythonhosted.org/packages/e6/00/63fda451b8bffa5808fc8bb311ee7c073b340974b09f273c7b2a145d3d62/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", size = 490347, upload-time = "2026-10-04T16:30:07.156Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/c093ffd070ba0fb02f76c565d06fecc65ad6e4afdbae78f7031076d3cdac/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", size = 392962, upload-time = "2026-10-04T16:30:08.77Z" }, + { url = "https://files.pythonhosted.org/packages/22/9d/d08a1128ab199b2f0cf25bfeb0639bd05119fff4b7c47bec24ef9a8ec23f/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", size = 371623, upload-time = "2026-10-04T16:30:10.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/c7/4758ddcbb75609414bbccfcb11d612436f9b3ee821bd2f33f0f1604ee648/rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", size = 376997, upload-time = "2026-10-04T16:30:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/87/e4/947bd7f608ff60faf46dc9d389c3dffd0e3d767d78a0be19978448ef0ce7/rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", size = 400169, upload-time = "2026-10-04T16:30:13.804Z" }, + { url = "https://files.pythonhosted.org/packages/4b/35/fe93e020a0543b5670472c18d7e6af3197c08da571240ce1965c84f85c0f/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", size = 551615, upload-time = "2026-10-04T16:30:15.332Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4f/5d2a0136bb03b2a56a39dc6ff92d58a6e3e53a2e17079238b86228882f16/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", size = 614655, upload-time = "2026-10-04T16:30:16.92Z" }, + { url = "https://files.pythonhosted.org/packages/09/1c/3f1025aaf70d9bf7272cc41f8b64ee76b48bf01726248138430e16f23b38/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", size = 579602, upload-time = "2026-10-04T16:30:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/53/0d/5c72e6204f76610608706da32b6b7e11ef7e10317558a7bb15a122e008dc/rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", size = 206315, upload-time = "2026-10-04T16:30:20.05Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0b/489d48abbcc7d70cf3fbf662d9d22abf1f4650761c0a9ae05260800800d3/rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", size = 222852, upload-time = "2026-10-04T16:30:21.604Z" }, + { url = "https://files.pythonhosted.org/packages/91/16/bbb05a7e6a10cf79ba639be7f799d770ee15f64175cc61d081b218dd402a/rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", size = 218709, upload-time = "2026-10-04T16:30:23.036Z" }, + { url = "https://files.pythonhosted.org/packages/22/ac/ac507a0a4ec478ca470440a09583db4be5259ba7670aeba0620822f1e57a/rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", size = 349596, upload-time = "2026-10-04T16:30:24.558Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f2/817a46b658d5070f477f722c298ee9a24525b0e4017347964146ef5fdd0e/rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", size = 343114, upload-time = "2026-10-04T16:30:26.048Z" }, + { url = "https://files.pythonhosted.org/packages/6c/42/6ade976b13ac1b4cb3bf2eb603f1be2fe74df19a29988d4c2b386be59d6f/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", size = 374197, upload-time = "2026-10-04T16:30:27.699Z" }, + { url = "https://files.pythonhosted.org/packages/d9/70/77cdf1d3f1a07faabe936016ae623aec7981f73108a8fe7a203ed2e21998/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", size = 378025, upload-time = "2026-10-04T16:30:29.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/18a44a3beaa9b7931acb04af7bd9539836477c630a794452d4826d6185d4/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", size = 491969, upload-time = "2026-10-04T16:30:30.995Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/fb39cfd6bddaf741b024f813890374578ff8ac1f1adc473659c048b03b05/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", size = 393905, upload-time = "2026-10-04T16:30:32.628Z" }, + { url = "https://files.pythonhosted.org/packages/ed/71/0fa7bb77b57af0d710273964180d11b503f62b8a5c358eb2d8c3f62feff6/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", size = 374569, upload-time = "2026-10-04T16:30:34.257Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/f41cfac269af3b449513ef1bc3d7f32fde52abbbd2d01c7e76b47743acd9/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", size = 377482, upload-time = "2026-10-04T16:30:35.997Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/312b49006e7f8f9ca5f96647577b8aa6f3df30519c46bd448f5c425af0b2/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", size = 400040, upload-time = "2026-10-04T16:30:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a6/18cca7a878dc7fa95165a83343fd4d7b65643fd22e54e47340a451121d5c/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", size = 551849, upload-time = "2026-10-04T16:30:39.443Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/1f5685e20f39604691bdc3c05aaa6b8bd2f954e9e996477adf2376768e33/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", size = 614842, upload-time = "2026-10-04T16:30:41.231Z" }, + { url = "https://files.pythonhosted.org/packages/c6/25/98652109fd9f7e10268dd4571aa52b81987001b806f37ef1a18260de714a/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", size = 582606, upload-time = "2026-10-04T16:30:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/11ca09099bab5f53373a80a334c424ec917c13d050760a59499d2af5171e/rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", size = 203341, upload-time = "2026-10-04T16:30:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8a/88909e3ffd9f47f5b58211473875d8c3c09079f0058c46fb72c55a702a26/rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", size = 222843, upload-time = "2026-10-04T16:30:46.486Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, + { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[package]] +name = "semgrep" +version = "1.180.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "boltons" }, + { name = "click" }, + { name = "click-option-group" }, + { name = "colorama" }, + { name = "exceptiongroup" }, + { name = "glom" }, + { name = "jsonschema" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-requests" }, + { name = "opentelemetry-instrumentation-threading" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "peewee" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "rich" }, + { name = "ruamel-yaml" }, + { name = "ruamel-yaml-clib" }, + { name = "semantic-version" }, + { name = "tomli" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/aa/34eac121a4b1859aae125aa8c997bbec1f00b05d3225ba4becff6d3685a1/semgrep-1.180.0.tar.gz", hash = "sha256:14aee785d0bc9656027a33fca7f97e88e089dbc6c7ed3df47f454b0584b0a3d0", size = 518569, upload-time = "2026-10-07T23:22:36.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/8ca81606f09f2c33630409f7e1f2ad89c5841f5da062eec8452cee6d51d7/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:f074dcfd6a56095fde32c354af27ef0cce6d15607bef42fa50964dd7042b667d", size = 45810903, upload-time = "2026-10-07T23:24:26.336Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/e29003080506e7666ba8225df63632f1ac6417764d4ee991e5dae04f2c0c/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:9c61070eaa7de785a8318e1d00de6c9151efa660cc3e58c3e2ffd6f5e6180af7", size = 49794130, upload-time = "2026-10-07T23:24:30.244Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/66135de6020e792491b0973d95efc89ef729a38c58106394b2798bf58dda/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:cba06f64b10e5ec86a0b3a9757cfdfdf3ce6c2bbdb5e98762aa2a4b0a296f8ca", size = 72563288, upload-time = "2026-10-07T23:24:34.008Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1f/f4af9cc92651f04ab973eac214a62ed23260da9e0696c105b4b9734da453/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:16db2e2706ab814a975eeec25debfad49abba846b7c513d88e041447152f8447", size = 70564254, upload-time = "2026-10-07T23:24:38.551Z" }, + { url = "https://files.pythonhosted.org/packages/37/c5/e712830b05b596fee47c4c8e5382c0439c2ac3aa46272f27818479216f38/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:1bab359e562f0af00217f9f827531c2e1375499b8559c90bed9690007e82565d", size = 79472194, upload-time = "2026-10-07T23:24:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/a1/40/21fefcbf9fe3d9670e98504f71a0974805812bbb308ddee5a31a7c5ba12b/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:d19b447b5d49c435dda099fd5f9d96ec7822e98a8ea6df16630f4f0596ed27ed", size = 77124939, upload-time = "2026-10-07T23:24:47.921Z" }, + { url = "https://files.pythonhosted.org/packages/82/75/3e3a75297e8f181e0a32b5691ee55c78a214d4a72a0370841f9c7674a5da/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:b403865e64f5ba0691630adf9bc13592cc38830726d4a114960d7e2d3fb21fc6", size = 58108036, upload-time = "2026-10-07T23:24:52.232Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/be/0123026f719d1a7936f214a88b553bb5701e04ff2511147c1dab0c5035eb/sse_starlette-3.5.0.tar.gz", hash = "sha256:75de713aa8a9441513cc283220826da079d982770965b951e9437720e8bafdb2", size = 36794, upload-time = "2026-09-28T17:48:14.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/e4/cdda14023c316d71493bc54fdffc3dd006631b88866145c9d3cc33e0f1df/sse_starlette-3.5.0-py3-none-any.whl", hash = "sha256:3e6e1070df3f0f5d9cea81496de92dbb72f6721871d99748ece67441dd8b7997", size = 17407, upload-time = "2026-09-28T17:48:13.228Z" }, +] + +[[package]] +name = "starlette" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/0c/6efb252d091ecccd7d62048ae11f0ea35cd75a4fbaeea5e30f9c3bf91d10/starlette-1.8.0.tar.gz", hash = "sha256:1565dc0b35d5737a271ed1e0e04e949f4e81198799f216d2667b0a0fb9cf9522", size = 2730457, upload-time = "2026-10-13T07:54:39.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b0/5742e4ac7af5eb58ec3470a537a49d7aa507e5539413e504b3a65ef50ba8/starlette-1.8.0-py3-none-any.whl", hash = "sha256:dfdd6b29c26483288088d990eee59631dedadd66ce20d203402a7ca8e3c4656f", size = 79612, upload-time = "2026-10-13T07:54:38.019Z" }, +] + +[[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/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 = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.54.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, +] + +[[package]] +name = "wcmatch" +version = "8.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/c4/55e0d36da61d7b8b2a49fd273e6b296fd5e8471c72ebbe438635d1af3968/wcmatch-8.5.2.tar.gz", hash = "sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2", size = 114983, upload-time = "2024-05-15T12:51:08.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/78/533ef890536e5ba0fd4f7df37482b5800ecaaceae9afc30978a1a7f88ff1/wcmatch-8.5.2-py3-none-any.whl", hash = "sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478", size = 39397, upload-time = "2024-05-15T12:51:06.2Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/23/655a1802fe8041302c959774ca7c80b53bc24737ff3ef45cb50ef11bd96c/zipp-4.1.1.tar.gz", hash = "sha256:7ebb7a44c021b29fd8dbd7cce6812d0d7b5b454521f93cc71af6ccd155aaa70b", size = 27649, upload-time = "2026-10-03T17:03:03.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/98/df615823cd9419131ce19fba00de53a663794369e198aade064a244b385d/zipp-4.1.1-py3-none-any.whl", hash = "sha256:8979f52d874162f485ff2981e3891f3a3317b7a3dd43ff1e1775b9304f307a9c", size = 10582, upload-time = "2026-10-03T17:03:02.506Z" }, +] diff --git a/uv.lock b/uv.lock index a96f4cd..0bd66cd 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,6 @@ version = 1 revision = 5 -requires-python = ">=3.14" - -[manifest] -overrides = [{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.15.1,<3" }] +requires-python = ">=3.15" [[package]] name = "actionlint-py" @@ -11,241 +8,6 @@ version = "1.7.12.25" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/33/75/36ee89db39bb3620d1e46fe1f6ec77d6000d30ff45345699e51b4c850e83/actionlint_py-1.7.12.25.tar.gz", hash = "sha256:83f83be4cd317c79b15cbb7022787513a6994f00bfb2dd8da02fa4e62b4f640d", size = 12438, upload-time = "2026-09-22T16:24:42.477Z" } -[[package]] -name = "annotated-types" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, -] - -[[package]] -name = "anyio" -version = "4.15.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.15'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, -] - -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - -[[package]] -name = "boltons" -version = "21.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ad/1f/6c0608d86e0fc77c982a2923ece80eef85f091f2332fc13cbce41d70d502/boltons-21.0.0.tar.gz", hash = "sha256:65e70a79a731a7fe6e98592ecfb5ccf2115873d01dbc576079874629e5c90f13", size = 180201, upload-time = "2021-05-17T01:20:17.802Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/a7/1a31561d10a089fcb46fe286766dd4e053a12f6e23b4fd1c26478aff2475/boltons-21.0.0-py2.py3-none-any.whl", hash = "sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b", size = 193723, upload-time = "2021-05-17T01:20:20.023Z" }, -] - -[[package]] -name = "bracex" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/01/5f394b8bcd6e5b92f73130990960423bbb19711f906bd9fe9ea5557c667c/bracex-3.0.1.tar.gz", hash = "sha256:4e38e32392e4a4780fe15d644bfc7c8514057cfc3861e060b11814ce829c25e4", size = 44019, upload-time = "2026-07-20T13:43:00.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/8f/6f7273a7adb8d73fc8d21ede4376a3e475e52f98435c6007f69100dec8ca/bracex-3.0.1-py3-none-any.whl", hash = "sha256:6523ad83aeb5098a4ee597cff0f964442ff74e460bd3fafaffab6a013ff2288c", size = 11940, upload-time = "2026-07-20T13:42:59.268Z" }, -] - -[[package]] -name = "certifi" -version = "2026.7.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, -] - -[[package]] -name = "cffi" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, - { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, - { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, - { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, - { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, - { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, - { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, - { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, - { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, - { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, - { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, - { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, - { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, - { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, - { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, - { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, - { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, - { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, - { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, - { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, - { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, - { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, - { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, - { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" }, - { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" }, - { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" }, - { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" }, - { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" }, - { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" }, - { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" }, - { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" }, - { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" }, - { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" }, - { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" }, - { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" }, - { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" }, - { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" }, - { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" }, - { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" }, - { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" }, - { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" }, - { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" }, - { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" }, - { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" }, - { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" }, - { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" }, - { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" }, - { url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" }, - { url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" }, - { url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" }, - { url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" }, - { url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" }, - { url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" }, - { url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" }, - { url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" }, - { url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" }, - { url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" }, - { url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" }, - { url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" }, - { url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" }, - { url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" }, - { url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" }, - { url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" }, - { url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" }, - { url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" }, - { url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" }, - { url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, - { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, - { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, - { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, - { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, - { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, - { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, - { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, -] - -[[package]] -name = "click" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, -] - -[[package]] -name = "click-option-group" -version = "0.5.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -255,183 +17,6 @@ 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 = "cryptography" -version = "50.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, - { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, - { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, - { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, - { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, - { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, - { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, - { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, - { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, - { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, - { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, - { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, - { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, - { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, - { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, - { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, - { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, - { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, - { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, - { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, - { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, - { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, - { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, - { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, - { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, - { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, - { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, - { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, - { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, - { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, - { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, - { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, -] - -[[package]] -name = "face" -version = "26.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "boltons" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/fd/f84f0600bd72953d5a322f0dedbd4f900e2cedab718e6b6a093ae2d16aae/face-26.0.1.tar.gz", hash = "sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6", size = 51754, upload-time = "2026-06-17T23:14:39.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/24/0159c48d19c8b05e6969ee809bbbecc5a5c863f7e38c9327e2c63cb06f0f/face-26.0.1-py3-none-any.whl", hash = "sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d", size = 57572, upload-time = "2026-06-17T23:14:38.711Z" }, -] - -[[package]] -name = "glom" -version = "25.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "boltons" }, - { name = "face" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/74/8387f95565ba7c30cd152a585b275ebb9a834d1d32782425c5d2fe0a102c/glom-25.12.0.tar.gz", hash = "sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069", size = 196128, upload-time = "2025-12-29T06:29:07.274Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.75.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/2b/6ce81972d5c8cab9705fddce3153be63222d9e12fd96f8baba5038a744dd/googleapis_common_protos-1.75.5.tar.gz", hash = "sha256:c7a866fc34ed29a3b10af627a4b9b1dc2433313ca6e959f0ae4feb132047ed72", size = 156513, upload-time = "2026-09-29T19:26:14.863Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/b9/6b29500a1c581ff4d77fd83c6568d068bee06f1b139fb6eb0a4f2d4bce8a/googleapis_common_protos-1.75.5-py3-none-any.whl", hash = "sha256:d7285525c23039db98f2463e6d5a4f9b958b94d497f03a844ece3259c4e72d5d", size = 307737, upload-time = "2026-09-29T19:25:48.735Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - -[[package]] -name = "idna" -version = "3.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - [[package]] name = "iniconfig" version = "2.3.1" @@ -441,40 +26,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, ] -[[package]] -name = "jsonschema" -version = "4.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - [[package]] name = "la-stack-scripts" version = "0.4.6" -source = { editable = "." } -dependencies = [ - { name = "research-repo-tools" }, -] +source = { virtual = "." } [package.dev-dependencies] dev = [ @@ -482,7 +37,6 @@ dev = [ { name = "pytest" }, { name = "research-repo-tools" }, { name = "ruff" }, - { name = "semgrep" }, { name = "shellcheck-py" }, { name = "shfmt-py" }, { name = "ty" }, @@ -493,202 +47,19 @@ tooling = [ ] [package.metadata] -requires-dist = [{ name = "research-repo-tools", specifier = "==0.1.8" }] [package.metadata.requires-dev] dev = [ { name = "actionlint-py", specifier = "==1.7.12.25" }, { name = "pytest", specifier = "==9.1.1" }, - { name = "research-repo-tools", specifier = "==0.1.8" }, + { name = "research-repo-tools", specifier = "==0.1.9" }, { name = "ruff", specifier = "==0.17.0" }, - { name = "semgrep", specifier = "==1.180.0" }, { name = "shellcheck-py", specifier = "==0.11.0.1" }, { name = "shfmt-py", specifier = "==4.2.0" }, - { name = "ty", specifier = "==0.0.86" }, + { name = "ty", specifier = "==0.0.87" }, { name = "yamllint", specifier = "==1.38.0" }, ] -tooling = [{ name = "research-repo-tools", specifier = "==0.1.8" }] - -[[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 = "mcp" -version = "1.29.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, -] - -[[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 = "opentelemetry-api" -version = "1.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-proto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.58b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/36/7c307d9be8ce4ee7beb86d7f1d31027f2a6a89228240405a858d6e4d64f9/opentelemetry_instrumentation-0.58b0.tar.gz", hash = "sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705", size = 31549, upload-time = "2025-09-11T11:42:14.084Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019, upload-time = "2025-09-11T11:41:00.624Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-requests" -version = "0.58b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/42/83ee32de763b919779aaa595b60c5a7b9c0a4b33952bbe432c5f6a783085/opentelemetry_instrumentation_requests-0.58b0.tar.gz", hash = "sha256:ae9495e6ff64e27bdb839fce91dbb4be56e325139828e8005f875baf41951a2e", size = 15188, upload-time = "2025-09-11T11:42:51.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/4d/f3476b28ea167d1762134352d01ae9693940a42c78994d9f1b32a4477816/opentelemetry_instrumentation_requests-0.58b0-py3-none-any.whl", hash = "sha256:672a0be0bb5b52bea0c11820b35e27edcf4cd22d34abe4afc59a92a80519f8a8", size = 12966, upload-time = "2025-09-11T11:41:52.67Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-threading" -version = "0.58b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/a9/3888cb0470e6eb48ea17b6802275ae71df411edd6382b9a8e8f391936fda/opentelemetry_instrumentation_threading-0.58b0.tar.gz", hash = "sha256:f68c61f77841f9ff6270176f4d496c10addbceacd782af434d705f83e4504862", size = 8770, upload-time = "2025-09-11T11:42:56.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/54/add1076cb37980e617723a96e29c84006983e8ad6fc589dde7f69ddc57d4/opentelemetry_instrumentation_threading-0.58b0-py3-none-any.whl", hash = "sha256:eacc072881006aceb5b9b6831bcdce718c67ef6f31ac0b32bd6a23a94d979b4a", size = 9312, upload-time = "2025-09-11T11:41:58.603Z" }, -] - -[[package]] -name = "opentelemetry-proto" -version = "1.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.58b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, -] - -[[package]] -name = "opentelemetry-util-http" -version = "0.58b0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411, upload-time = "2025-09-11T11:43:05.602Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" }, -] +tooling = [{ name = "research-repo-tools", specifier = "==0.1.9" }] [[package]] name = "packaging" @@ -708,15 +79,6 @@ 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 = "peewee" -version = "3.19.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/b0/79462b42e89764998756e0557f2b58a15610a5b4512fbbcccae58fba7237/peewee-3.19.0.tar.gz", hash = "sha256:f88292a6f0d7b906cb26bca9c8599b8f4d8920ebd36124400d0cbaaaf915511f", size = 974035, upload-time = "2026-01-07T17:24:59.597Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/41/19c65578ef9a54b3083253c68a607f099642747168fe00f3a2bceb7c3a34/peewee-3.19.0-py3-none-any.whl", hash = "sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417", size = 411885, upload-time = "2026-01-07T17:24:58.33Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -726,131 +88,6 @@ 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 = "protobuf" -version = "6.33.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, - { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, - { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, - { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, -] - -[[package]] -name = "pycparser" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" }, -] - -[[package]] -name = "pydantic" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6b/fb/6e44b63b26efea1cec48c26d8362313310202ef5ed6e7a52f1669e64e2cd/pydantic-2.14.0.tar.gz", hash = "sha256:8a51a7aaddd60f55566d1f07bdd87b92b463903f39a8f26b71a06314cd1548ae", size = 898613, upload-time = "2026-10-08T14:34:48.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/eb/9146591cc819d040475bf7f2be786710c7f7eb8083693bf859728da2ca9c/pydantic-2.14.0-py3-none-any.whl", hash = "sha256:15fab1bea6f1dc5003b54fc2ecab230c1fd1dbade2acd4addc52d81e32416d4b", size = 487109, upload-time = "2026-10-08T14:34:46.864Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e6/6d/196e8c819e0e934f35a1a33b3530396feadb0af4ca38fe9f995249e55794/pydantic_core-2.50.0.tar.gz", hash = "sha256:84d2d38f7d163c4dec292f379e9de1960c661795442aca6c90d706436cb3749e", size = 513355, upload-time = "2026-10-08T14:30:58.245Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/d9/6dd838672e5ccddf01556bfd1b4a6767e0c75abf9aaa4092cb87a56dbc4a/pydantic_core-2.50.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:62a93a9d206a3580c975c3c1f65a869cd063844d016c004f8d786a9309b3c591", size = 2224496, upload-time = "2026-10-08T14:27:39.281Z" }, - { url = "https://files.pythonhosted.org/packages/b3/20/c57d2efcc63fb8fb6ae9ec2818baf30e2a68751e8d1dedefaf3dab2e45fd/pydantic_core-2.50.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5a8403eef4a66743e339102fb3cdd8c8b9016b8bd67924685893d062c88896f9", size = 2023860, upload-time = "2026-10-08T14:27:41.332Z" }, - { url = "https://files.pythonhosted.org/packages/26/c8/f44ea3f1b00288f715e2320cbcd0f604104325370ef4bee42c5ed2ab076e/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8354fbbe2abf0fb724b9303bef43bfd9b7a2779332813fa6d6559983954e7d8", size = 2049144, upload-time = "2026-10-08T14:27:43.695Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/2f5cf84ceaf6d7c351737124aebc135f50ce48a9dd28d2b5283103face54/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00963fde61cf8880d9e7b5635a9830e0591edfa45168447fdc5b47635c0f6437", size = 2173456, upload-time = "2026-10-08T14:27:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/97/de/dc0bd815a328e62b72bb93b5f3c762939670d4119fd27405b454970833da/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bbd7da16d8b2912cc56c9d0b85c4998a6cbf39b220f81c0d8c397c64672ae0e", size = 2365204, upload-time = "2026-10-08T14:27:48.117Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a5/458c4a29f52fdecb16b192fb27f117359da9c9c58401ebfd956637822dca/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed557fa2744617eac3e34dd39b85037efbf23cd33f06851c00fdb8f18ad8f4c2", size = 2445007, upload-time = "2026-10-08T14:27:50.637Z" }, - { url = "https://files.pythonhosted.org/packages/98/14/0c0e72e0663be91a456167a2f7ed49c65195b388e8f1e8b2bb5083b27458/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f816dc04e99627a5a6352ae51dfb30e603f2cb0b7009c91dc640c533def014", size = 2159911, upload-time = "2026-10-08T14:27:52.881Z" }, - { url = "https://files.pythonhosted.org/packages/62/7f/64af6921e17ed04dda0e07ebbd394b29fb557d33618cb0f3f81ec59c0508/pydantic_core-2.50.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:779b6c74526596a86d38248dedaecfb7851bbaf319c234be042c57acddd2c8c4", size = 2235761, upload-time = "2026-10-08T14:27:55.194Z" }, - { url = "https://files.pythonhosted.org/packages/82/11/b6ff9f7207af629094f4deac947d04be3812d5c95054010ad8f710a052f0/pydantic_core-2.50.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:21b62d45f327eb0802f842c132fda6d01a8376a8077922dc4dda69011c64d34a", size = 2243726, upload-time = "2026-10-08T14:27:57.336Z" }, - { url = "https://files.pythonhosted.org/packages/8e/37/6f7101f3c746a1686987a32875ae2f7c19f518d783979cee1a8a32e0a9ef/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f31af62efd1fd0257735b72e6716b32d4f207654adeabfe524d44baf1bb6bed", size = 2277158, upload-time = "2026-10-08T14:27:59.714Z" }, - { url = "https://files.pythonhosted.org/packages/d2/74/67fe208f3ec7f5d8c47bf7cba70019a84b1cffad947ebc8caef14271b54f/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:933f2d639eb81a3e1f145aec415453cc00983236629933f028d9507222583a2e", size = 2450059, upload-time = "2026-10-08T14:28:02.17Z" }, - { url = "https://files.pythonhosted.org/packages/cd/10/4a9c56a69f5841bdd94ffff5876a12ad2733209d5b89cadb67638d69e7c7/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:c05d035e72530f6b00297941b0162218601542b76870c3bf2756bd87f16fc538", size = 2479142, upload-time = "2026-10-08T14:28:04.494Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c1/30d36746051e42bb67df986ee874f787526edb99205f6b1583c33dd81202/pydantic_core-2.50.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab3f95f737fc1b258b8210308fc02ce1442cf5950cf6d30b09ad89ab9e8afebd", size = 1483929, upload-time = "2026-10-08T14:28:06.527Z" }, - { url = "https://files.pythonhosted.org/packages/04/2c/c0e8949be4f99a74f02ae02d603bdd518c8a9eba72982a34d675134a792a/pydantic_core-2.50.0-cp314-cp314-win32.whl", hash = "sha256:f12d9690634414fc04b1a7072fdc35c34a9242232c1851fe4518383578bb09d4", size = 2104255, upload-time = "2026-10-08T14:28:08.729Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6f/a5a5baf99509c998e0f2335d66b815880c7d6c63231c8e4017b237d6837d/pydantic_core-2.50.0-cp314-cp314-win_amd64.whl", hash = "sha256:63263d64884688554fb025a3906c7b00573980cfee75f9afeb86239d577384bc", size = 2170939, upload-time = "2026-10-08T14:28:11.08Z" }, - { url = "https://files.pythonhosted.org/packages/40/bc/c89b93b69d59cfe61c1e91b1cd3d87cc740e57327ecfef84ce34b7dcadac/pydantic_core-2.50.0-cp314-cp314-win_arm64.whl", hash = "sha256:753863dd4317ec8cc9eb3e6d9d01a8ef1a1726a8003b4354658d68a1ae05f9db", size = 2157035, upload-time = "2026-10-08T14:28:13.445Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5e/d0ad47a406c42db95876900f4bfc520313c28d07a0dd5080da4972220c27/pydantic_core-2.50.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c4abc425789f8540e86ca4cfdbc8dc650433cc2ac7c6136fee9bcb29d5665a02", size = 2234007, upload-time = "2026-10-08T14:28:15.9Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f4/69a7ec8400c1e34e5e11ab967f850ffd8ca71bb3aa0a8db93df5912bb6f2/pydantic_core-2.50.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf150693a51ca21e8288cd08a9de05e5ab331776dfcfd0537b14523338f0502a", size = 2000888, upload-time = "2026-10-08T14:28:17.983Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/860bb499f7cdf4bb453e8080af5bc182487f2f025d94baeee29c907bde6f/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:158749408ee19682b8a2a7e7135cced7f41d6f2f9de96088b1b1609858a6a158", size = 2022761, upload-time = "2026-10-08T14:28:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/89/fd/f571420436e79b9f7cc8f8ec534dee1759c240b4dc086737608f14d05c93/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcbd1fe5083315243447c13f7252ae9fe9d128b1ae2857e3a916c609235dd863", size = 2192910, upload-time = "2026-10-08T14:28:22.502Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/b47c4a01ea978a7a3d02551e3625f0344ae1352d67ea78011c2eca5e39e4/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fdeda6272d60b1f6fb63f6a3dade55e274af62e1514d24a6549a12150c385cc", size = 2390178, upload-time = "2026-10-08T14:28:24.861Z" }, - { url = "https://files.pythonhosted.org/packages/77/61/d109f26b3ee4443cca220eaaa4f3fbcf78ff07aeb577becc37d9224db63f/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b16e164205a90b1050d2f5469f8a7829db7698ad198c69e6aab6cbfb5648b87", size = 2464453, upload-time = "2026-10-08T14:28:27.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/a4/7be2f608f6f8e7655155057a82a7e2f135ad9b37fcbadd34fdf11b1ade24/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17a5ca9c197788a6424749a09a3dce824cb2c17b72f835f8a5e330935b973609", size = 2150682, upload-time = "2026-10-08T14:28:29.65Z" }, - { url = "https://files.pythonhosted.org/packages/80/ef/a8d867f6d3981c232d0f3c2254458cdee8471de7bff3f7edd252cc00c30c/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:01340f4fbb4f854b1f36a6fe9dcd2b26c8936aead4e4ad1205624ac025c875fc", size = 2245995, upload-time = "2026-10-08T14:28:31.934Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a6/7297a39c8814beab877be5ba4f594c2e166108cdd20c1f1c51fb71d5389e/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99e203d5c2a814facef78dcd993b0fb7a933124a3475a38979fc09cceae210b7", size = 2264090, upload-time = "2026-10-08T14:28:34.298Z" }, - { url = "https://files.pythonhosted.org/packages/c3/65/634fc407eaf61abec0d015d6fd4fd8456c18c71358e984f60b0a05092d49/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:2092ce156f92aff17e3345baba2b7c0c1701f32ba922c5de71bd6248fbe164e3", size = 2293708, upload-time = "2026-10-08T14:28:36.725Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/b87fcf8062b8907c77769385a4340a814fe359660593dbbe4d8f3f7e9e61/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:74cbb6cbd74445ca279668790e0c10eccac0428fd79fe061fce6c9e3982ad3fe", size = 2468464, upload-time = "2026-10-08T14:28:39.109Z" }, - { url = "https://files.pythonhosted.org/packages/77/f4/d7aacec95f9e00dcbad84bee2fd0fc081ae292d380dc8a3268e4103de957/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:42fff617cb0b08505d8123d71e6e7a8e54210d9007498f564855bd843ce984b1", size = 2511897, upload-time = "2026-10-08T14:28:41.681Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d8/0a755b0069f9a0f4c57f98fd05b524473952556406244649d632d25f8646/pydantic_core-2.50.0-cp314-cp314t-win32.whl", hash = "sha256:36f9ed6ae1069913e4f6e86d8233e119e83e00a20c54f88faf9c81292f2fecc0", size = 2098906, upload-time = "2026-10-08T14:28:44.218Z" }, - { url = "https://files.pythonhosted.org/packages/27/4e/cd10a1fbd1ba1d730871e382ec2ac550487465c7c9be1a72b3f4f9181817/pydantic_core-2.50.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c81c2ec53ea9eb2227c14b3e6de35670b2de163b638f2a803e90a3bd5bbb0", size = 2148359, upload-time = "2026-10-08T14:28:46.47Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b1/a766eadfbc16b58d401f63dde3bf4e0943dcd2402011b6b8d9de2031ebe0/pydantic_core-2.50.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dec0dafc116ac29a84d143fdbc3b83fdb5d4ed339276be2251154537ab30e14d", size = 2175241, upload-time = "2026-10-08T14:28:48.934Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/43d0e765a80d6fc7d87b5f66a9023777aad6da7e7d64f486a4c2be8158b7/pydantic_core-2.50.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:af2b808a79bb04075e87c81a5b6179365b93f9a851f29dafd67abff72085d0c8", size = 2224750, upload-time = "2026-10-08T14:28:51.227Z" }, - { url = "https://files.pythonhosted.org/packages/9c/12/07e047c21ad90f184c7e8bfc6c9966b9ca51d4698e1a06460b65ca2ce105/pydantic_core-2.50.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2918547195ffb20118b829fdb8938e9dc92c9527fe6cbe58572594c96362880e", size = 2023936, upload-time = "2026-10-08T14:28:53.978Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a9/fb26fdd343ead65a699245c9d2150e84c47496a9b250859214a90cceecc9/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0393763d66f6f61715488d074a2cefac04aeb3ee281e36fb4925dd44deaf9e17", size = 2048872, upload-time = "2026-10-08T14:28:56.342Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4d/9fc4ea28a8ecc1b85fe77a616f8df9a51696476aaa93c5b256269d025044/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62ec6568896e0abf258cbcd22c406c1c8bf27d16224b21bec8f75c4ae88a8173", size = 2173752, upload-time = "2026-10-08T14:28:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/70/7e/74b55196339413d787b283c2568cca4679b0645e48229421228acfa8839f/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec786cb9d597dd75d993f8c1273e31bb2114c9bc22f67fba611e654e8347701b", size = 2365284, upload-time = "2026-10-08T14:29:01.434Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0e/4ebc7a851f0ef63fcaba205739c171e50dd183f8d79dcf0099c3b46aa60b/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fce74add1099da1ea09473268950270071fd56773e2968604efb3ab1d240e02", size = 2445279, upload-time = "2026-10-08T14:29:04.034Z" }, - { url = "https://files.pythonhosted.org/packages/07/09/03d3524fc7d4960840e32fd88c8cf487b5516daa7b6845ffc74274e0247a/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:243088c95e23b12db9f2cd7661d584a3f00814e087489f40cde7f9feac56b694", size = 2160424, upload-time = "2026-10-08T14:29:06.628Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b9/7c530c84049089033dd746700bfbb51520ed9376bc90f3dbbcb319880acf/pydantic_core-2.50.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:42a56b0052ac11d9d0d87b1c94a1ce52e31914fd133f269585e0f63a4ed988f2", size = 2236109, upload-time = "2026-10-08T14:29:09.053Z" }, - { url = "https://files.pythonhosted.org/packages/0a/08/70e07379ebc2538c22f8c372a37c009b96a3a1bd7921aee1dad28ee41be0/pydantic_core-2.50.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:350001f5573451150d919722ee095aa28bec037d6e23b86d7f04581a910fe924", size = 2244011, upload-time = "2026-10-08T14:29:11.549Z" }, - { url = "https://files.pythonhosted.org/packages/66/49/294810baacae4b088bfac60edab06036951e5ae6e0c20e8f6cfef7d70f66/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:21e38a011783d8afc8b9d79928e273d06f349b52ae84edf63ec18ad07f077484", size = 2278392, upload-time = "2026-10-08T14:29:13.998Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0e/6990b812f124cf3564b36492799df7a66cd3cf5678004ffe65dda85b136b/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:588d309ce5c85448379556d72011b191c0414ee2e80d7c3f1ebe2ceb2d9027b1", size = 2450012, upload-time = "2026-10-08T14:29:16.524Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c4/f8f1a763550a077c51d2c69b7a32db56d1b54e61c613bc19e6c7fd2e7cb2/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:a8ee3965e01f10e4ff92ba1727626328ab7ba93bcd5674ff2b78ea1048ee0cea", size = 2479233, upload-time = "2026-10-08T14:29:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/9d/83/7a09baa1e0e4710be47b84aaf808e7b86247d7fa43251ac8934099113f8d/pydantic_core-2.50.0-cp315-cp315-win32.whl", hash = "sha256:c05b75ef3574c9ee4e05bbcf8513f7ccb155d426514be9efef5f6f53152d5f5c", size = 2104507, upload-time = "2026-10-08T14:29:21.694Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/f1cf61f747538834ea2c14c442264e4f29663c95294ad7933867633a1dea/pydantic_core-2.50.0-cp315-cp315-win_amd64.whl", hash = "sha256:8447678b49412294801c9ea15ae31ea3bae7da65425268c92d40e032c38eac6f", size = 2171379, upload-time = "2026-10-08T14:29:24.613Z" }, - { url = "https://files.pythonhosted.org/packages/30/c8/9871261b760672cfa334498d94bc7dcbff2495345e639e806127f7087254/pydantic_core-2.50.0-cp315-cp315-win_arm64.whl", hash = "sha256:92016718bcf3e6f35a6bd986880191a8da7a35aa1f5b1e97544582ef938464cf", size = 2157182, upload-time = "2026-10-08T14:29:27.082Z" }, - { url = "https://files.pythonhosted.org/packages/de/3c/5107269aee5ee7855fa370ff837998792dbf124b8545244c370bec251a66/pydantic_core-2.50.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:71061800a3c225e730f7997f8a7fad6e0d0dcbe36609cdc7e60576a099a2832b", size = 2234440, upload-time = "2026-10-08T14:29:29.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/cb179b0c404337ef517515539f9899164a6d3d43d5c79c2556165da211c1/pydantic_core-2.50.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bc87bd34239835c8d73171acd039e591cea0a2ca8615e6188044ad170a40fca1", size = 2001074, upload-time = "2026-10-08T14:29:32.327Z" }, - { url = "https://files.pythonhosted.org/packages/b0/59/ef1714204f145e9497ab71e8bd82c5e40b5adf365d420be777d1e047cdaf/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57c0e5b26d82bf31ab2b044781527b1b1a36e9ed400856b6aca5097eb1abb909", size = 2023016, upload-time = "2026-10-08T14:29:35.06Z" }, - { url = "https://files.pythonhosted.org/packages/87/e9/08a3ade34b4af09a83740b62a4442b7623127d388fc21d4938798dd85a26/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41b9f2821f0a105f88cd54ad03fe1392ec600618ffa8610f7c5e466ecd98c531", size = 2193165, upload-time = "2026-10-08T14:29:37.721Z" }, - { url = "https://files.pythonhosted.org/packages/2a/56/29aa3e540d3aca72139e5b7dc56395a940849d85da2b7a9ccfbff71d0928/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:364f62d8024997536e57865cb1c8b36effad3249bf392a29a2379ea69db28238", size = 2390198, upload-time = "2026-10-08T14:29:40.616Z" }, - { url = "https://files.pythonhosted.org/packages/76/63/a5cbdde0a2090c47002819d85317428e15b41d22e581cf38e9714b4903af/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:955d7878130dfc124d6a5343e1b87d2933244fe14a4a0a3e98787e8e660a8eb4", size = 2464777, upload-time = "2026-10-08T14:29:43.29Z" }, - { url = "https://files.pythonhosted.org/packages/c5/55/fff3a363b0dcf72fdd72311c676b1fa769f2c13342129c694b2eb9886580/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf81432281af66ba3285b26b09c2d478d84730dc50ff90926c1bdcef54048a33", size = 2150389, upload-time = "2026-10-08T14:29:46.271Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f3/0062361fd2377185d322dc84cd05ec4b96da352195c89939c7b509664721/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:34a4a0938eb30931baca56e55786c5a6871ac7aa891a38c8cabcdb7e49dab91b", size = 2246225, upload-time = "2026-10-08T14:29:49.173Z" }, - { url = "https://files.pythonhosted.org/packages/f9/45/662e3870143d5f1632a02ceb9ce2d5168e88caf3da80e9b42f495ceb1284/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f517a417cd02aa8fb05b603a0ac6d87b7b004c3ba4fcd279cad25cab7229043b", size = 2264404, upload-time = "2026-10-08T14:29:51.699Z" }, - { url = "https://files.pythonhosted.org/packages/32/5e/8b14ebf111700362c6e519e40209d86f521da81ef171b2891af0bb5de311/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:d01029d54ff1f45c195b1f7e6fbf6e58fb7e7e12cda9a6e639570d9c581decb2", size = 2294152, upload-time = "2026-10-08T14:29:54.349Z" }, - { url = "https://files.pythonhosted.org/packages/55/29/85c486e0d25a8b523803032e97d7138edb7b7bc887fa8c6dd085e8824cdb/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d06dbbfe8da01a0574de27afc19915bb3e7dddfbd184bb97e958f94051d8531b", size = 2469160, upload-time = "2026-10-08T14:29:57.193Z" }, - { url = "https://files.pythonhosted.org/packages/12/bf/c451db7567e92d6601c4d2f6bdbb945cf60104182e2b241d8fe0b6f01414/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:5307a8bd49158b57a0ca4e10950d31085aa35d9007e934047043ccc6d28eea97", size = 2512204, upload-time = "2026-10-08T14:30:00.177Z" }, - { url = "https://files.pythonhosted.org/packages/43/1e/5b93a2513ced099acb0dc4dd6684742d6564ade60a59abcbabd161624e07/pydantic_core-2.50.0-cp315-cp315t-win32.whl", hash = "sha256:c2b246fa7cbdf9918488d1542a82bbb928cf71bcba66905c24131981e759ff0b", size = 2099026, upload-time = "2026-10-08T14:30:03.154Z" }, - { url = "https://files.pythonhosted.org/packages/37/f5/1b5967e31b025a23f2f627ffea1f5a874cff0adb72fa5f5af6bb36394ad9/pydantic_core-2.50.0-cp315-cp315t-win_amd64.whl", hash = "sha256:36c49d4e1769127461b609f110d91963790091ffcc2de401ac1d3b2f6a63bd54", size = 2148456, upload-time = "2026-10-08T14:30:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/213d6fe84816e8a5ed7b7539ecebb3f14c20739c2469f4e372f0f15bb042/pydantic_core-2.50.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f3abcabb04503023e1053add24472e78878de0bfd5c8a93686be01f4032c363c", size = 2175368, upload-time = "2026-10-08T14:30:08.989Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, -] - [[package]] name = "pygments" version = "2.21.0" @@ -860,20 +97,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] -[[package]] -name = "pyjwt" -version = "2.15.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252, upload-time = "2026-09-28T18:40:42.598Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - [[package]] name = "pytest" version = "9.1.1" @@ -890,206 +113,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] -[[package]] -name = "python-dotenv" -version = "1.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.32" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, -] [[package]] name = "research-repo-tools" -version = "0.1.8" +version = "0.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pyyaml" }, { name = "rust-just" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/fd/f804affa973b85f455b20d5f129527fc2176f78bbc05ab710906f4242034/research_repo_tools-0.1.8.tar.gz", hash = "sha256:bc855aa930ee04301dceebc8340a22576bb25712e322b51d7afaf20f5fab1ba0", size = 765360, upload-time = "2026-10-09T04:38:42.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/2c/f37ffb902034866dd03c3f939c580f2d151e51955669c7d9bc4d00cf4858/research_repo_tools-0.1.8-py3-none-any.whl", hash = "sha256:9db45f09a33cac4ff32ddf010d30523e295f607e24c680bcfc3d3d9786991293", size = 339758, upload-time = "2026-10-09T04:38:39.806Z" }, -] - -[[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 = "rpds-py" -version = "2026.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/f2/67b94be1532767803415c1c5a1fd88ea487643d74a673cec1ba140af77bb/rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", size = 347178, upload-time = "2026-10-04T16:30:00.865Z" }, - { url = "https://files.pythonhosted.org/packages/04/37/b751de2b59b0197a1d92a5dd491de88e8a5e928c2e6562581974f1e85263/rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", size = 341878, upload-time = "2026-10-04T16:30:02.564Z" }, - { url = "https://files.pythonhosted.org/packages/72/e2/5873bc4643c250db9e05d48dc0c93763d4aa68bc3b81164cb1af3b45b284/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", size = 373963, upload-time = "2026-10-04T16:30:04.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/03/5acf7632158247f3f6386ff0af3a1ee48167d575037e8d0920594b76d92b/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", size = 377975, upload-time = "2026-10-04T16:30:05.555Z" }, - { url = "https://files.pythonhosted.org/packages/e6/00/63fda451b8bffa5808fc8bb311ee7c073b340974b09f273c7b2a145d3d62/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", size = 490347, upload-time = "2026-10-04T16:30:07.156Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ae/c093ffd070ba0fb02f76c565d06fecc65ad6e4afdbae78f7031076d3cdac/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", size = 392962, upload-time = "2026-10-04T16:30:08.77Z" }, - { url = "https://files.pythonhosted.org/packages/22/9d/d08a1128ab199b2f0cf25bfeb0639bd05119fff4b7c47bec24ef9a8ec23f/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", size = 371623, upload-time = "2026-10-04T16:30:10.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/c7/4758ddcbb75609414bbccfcb11d612436f9b3ee821bd2f33f0f1604ee648/rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", size = 376997, upload-time = "2026-10-04T16:30:12.124Z" }, - { url = "https://files.pythonhosted.org/packages/87/e4/947bd7f608ff60faf46dc9d389c3dffd0e3d767d78a0be19978448ef0ce7/rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", size = 400169, upload-time = "2026-10-04T16:30:13.804Z" }, - { url = "https://files.pythonhosted.org/packages/4b/35/fe93e020a0543b5670472c18d7e6af3197c08da571240ce1965c84f85c0f/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", size = 551615, upload-time = "2026-10-04T16:30:15.332Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4f/5d2a0136bb03b2a56a39dc6ff92d58a6e3e53a2e17079238b86228882f16/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", size = 614655, upload-time = "2026-10-04T16:30:16.92Z" }, - { url = "https://files.pythonhosted.org/packages/09/1c/3f1025aaf70d9bf7272cc41f8b64ee76b48bf01726248138430e16f23b38/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", size = 579602, upload-time = "2026-10-04T16:30:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/53/0d/5c72e6204f76610608706da32b6b7e11ef7e10317558a7bb15a122e008dc/rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", size = 206315, upload-time = "2026-10-04T16:30:20.05Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0b/489d48abbcc7d70cf3fbf662d9d22abf1f4650761c0a9ae05260800800d3/rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", size = 222852, upload-time = "2026-10-04T16:30:21.604Z" }, - { url = "https://files.pythonhosted.org/packages/91/16/bbb05a7e6a10cf79ba639be7f799d770ee15f64175cc61d081b218dd402a/rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", size = 218709, upload-time = "2026-10-04T16:30:23.036Z" }, - { url = "https://files.pythonhosted.org/packages/22/ac/ac507a0a4ec478ca470440a09583db4be5259ba7670aeba0620822f1e57a/rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", size = 349596, upload-time = "2026-10-04T16:30:24.558Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f2/817a46b658d5070f477f722c298ee9a24525b0e4017347964146ef5fdd0e/rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", size = 343114, upload-time = "2026-10-04T16:30:26.048Z" }, - { url = "https://files.pythonhosted.org/packages/6c/42/6ade976b13ac1b4cb3bf2eb603f1be2fe74df19a29988d4c2b386be59d6f/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", size = 374197, upload-time = "2026-10-04T16:30:27.699Z" }, - { url = "https://files.pythonhosted.org/packages/d9/70/77cdf1d3f1a07faabe936016ae623aec7981f73108a8fe7a203ed2e21998/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", size = 378025, upload-time = "2026-10-04T16:30:29.451Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/18a44a3beaa9b7931acb04af7bd9539836477c630a794452d4826d6185d4/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", size = 491969, upload-time = "2026-10-04T16:30:30.995Z" }, - { url = "https://files.pythonhosted.org/packages/73/27/fb39cfd6bddaf741b024f813890374578ff8ac1f1adc473659c048b03b05/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", size = 393905, upload-time = "2026-10-04T16:30:32.628Z" }, - { url = "https://files.pythonhosted.org/packages/ed/71/0fa7bb77b57af0d710273964180d11b503f62b8a5c358eb2d8c3f62feff6/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", size = 374569, upload-time = "2026-10-04T16:30:34.257Z" }, - { url = "https://files.pythonhosted.org/packages/54/22/f41cfac269af3b449513ef1bc3d7f32fde52abbbd2d01c7e76b47743acd9/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", size = 377482, upload-time = "2026-10-04T16:30:35.997Z" }, - { url = "https://files.pythonhosted.org/packages/b4/fc/312b49006e7f8f9ca5f96647577b8aa6f3df30519c46bd448f5c425af0b2/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", size = 400040, upload-time = "2026-10-04T16:30:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a6/18cca7a878dc7fa95165a83343fd4d7b65643fd22e54e47340a451121d5c/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", size = 551849, upload-time = "2026-10-04T16:30:39.443Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6d/1f5685e20f39604691bdc3c05aaa6b8bd2f954e9e996477adf2376768e33/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", size = 614842, upload-time = "2026-10-04T16:30:41.231Z" }, - { url = "https://files.pythonhosted.org/packages/c6/25/98652109fd9f7e10268dd4571aa52b81987001b806f37ef1a18260de714a/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", size = 582606, upload-time = "2026-10-04T16:30:43.345Z" }, - { url = "https://files.pythonhosted.org/packages/19/03/11ca09099bab5f53373a80a334c424ec917c13d050760a59499d2af5171e/rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", size = 203341, upload-time = "2026-10-04T16:30:44.954Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8a/88909e3ffd9f47f5b58211473875d8c3c09079f0058c46fb72c55a702a26/rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", size = 222843, upload-time = "2026-10-04T16:30:46.486Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b7/a662f367d4896dd0a10cef2fc91f10b7f08af1c10858e287e019563f338d/rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9", size = 347463, upload-time = "2026-10-04T16:30:48.203Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3f/ad45d03df4f84ebae5439577ee81f3999d182711e82235c8037b6528890e/rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25", size = 342051, upload-time = "2026-10-04T16:30:49.872Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/3dcd68c13d4bcc59c1f7eb33ac8e80698f06f3eb0d8a1e06419836071c20/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871", size = 374143, upload-time = "2026-10-04T16:30:51.508Z" }, - { url = "https://files.pythonhosted.org/packages/cf/0d/68c1f058a250fbd1380ebda9fc227cbf50117adf8ffe8161ef383ea79f68/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91", size = 378604, upload-time = "2026-10-04T16:30:53.206Z" }, - { url = "https://files.pythonhosted.org/packages/d7/d6/2d4c59b85397cb4800594fadf692688ccf5ce556adc930e7a5bf21061a5e/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f", size = 490790, upload-time = "2026-10-04T16:30:54.925Z" }, - { url = "https://files.pythonhosted.org/packages/da/04/7e05dc3aebaf52f4e026766bd668fdd09a9d0e23f64a14686b36b3501892/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d", size = 393146, upload-time = "2026-10-04T16:30:56.625Z" }, - { url = "https://files.pythonhosted.org/packages/57/ca/e2e9a0a46a74ed51a0498ba1fe10f4ea6b2d9a155f372a2f91e51f18cf10/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945", size = 372022, upload-time = "2026-10-04T16:30:58.295Z" }, - { url = "https://files.pythonhosted.org/packages/ba/cb/8f8774df5134e23424372838bcc5c7ed4127d723e1ff52f7bebd4dcb2563/rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297", size = 377092, upload-time = "2026-10-04T16:30:59.984Z" }, - { url = "https://files.pythonhosted.org/packages/90/02/8d7095d73bf9114219be40230baa5df00611e0a82ed9517779ff9c19f82b/rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0", size = 400830, upload-time = "2026-10-04T16:31:01.721Z" }, - { url = "https://files.pythonhosted.org/packages/ec/02/8206856f8f363cd042a8315dc86b3912f5dcb6d3c66bbb24b69c2bfb0775/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9", size = 551478, upload-time = "2026-10-04T16:31:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/41/6b/36211f1bb1f0b0313f496d92f5905b74ea107f27cb16fa3355a82f04575e/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6", size = 615173, upload-time = "2026-10-04T16:31:05.281Z" }, - { url = "https://files.pythonhosted.org/packages/35/77/cda0c4a6f055446b692f0ed5692f73707cafd3dff82672ede31b1a9b59de/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7", size = 579653, upload-time = "2026-10-04T16:31:07.065Z" }, - { url = "https://files.pythonhosted.org/packages/74/ec/d8385f446240aed643b9e92a5055cff3015cc04a13c61f73b2883c478ed5/rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429", size = 206498, upload-time = "2026-10-04T16:31:08.989Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a5/71b5cd00e0521e3b6b81828baea368c62b6b700ebdd9554cd7d41ddf12fa/rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15", size = 223121, upload-time = "2026-10-04T16:31:10.84Z" }, - { url = "https://files.pythonhosted.org/packages/33/58/dba857c3bc8221b31b62eb170a3080f4f191de79f047200389b7ed1b06a7/rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009", size = 218741, upload-time = "2026-10-04T16:31:12.592Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d0/320ab28ccc1415eeb509d68682b0014fb74690cd49f1c2d29a232475af50/rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0", size = 349719, upload-time = "2026-10-04T16:31:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/56/88/f5b12f1358f443c08b7ce3cc8391d82d335f4872580e2e097847fd36087a/rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a", size = 343267, upload-time = "2026-10-04T16:31:16.827Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0c/c765b0059d532acb3b9c45d781ccc22f15a96dbe443d00903f643ba9df10/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d", size = 374555, upload-time = "2026-10-04T16:31:18.931Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8a/cafddfda77564a10cd21184640c3bffda6a2b8d20972fb5dedd5e0166328/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db", size = 378346, upload-time = "2026-10-04T16:31:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/b9/01/5e626016eff72c183bf6c96539240cace15d402468647a453ec08415b2fc/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250", size = 492373, upload-time = "2026-10-04T16:31:22.614Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9c/15a2469e9389242f46896b3f0a01d68caea8a5a35c011fcb05ef333aae73/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f", size = 394371, upload-time = "2026-10-04T16:31:24.768Z" }, - { url = "https://files.pythonhosted.org/packages/63/f5/c100ff77e1e6366e947c75969c258fdfc4b7bc5bbfe7351e62ffbf2a1228/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018", size = 375132, upload-time = "2026-10-04T16:31:26.588Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f4/fe0269c9de253e99c81cabc12b8971a5feaa083debdaff1221e06264d9e3/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d", size = 377642, upload-time = "2026-10-04T16:31:28.677Z" }, - { url = "https://files.pythonhosted.org/packages/05/65/b34a7b257baccff8f4a24a722933166d4941d5ebdc9c3f4bc4ffcd5ce4f4/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b", size = 400855, upload-time = "2026-10-04T16:31:30.802Z" }, - { url = "https://files.pythonhosted.org/packages/98/32/844e54176b6071b90b38a564e6940bc6eb8f97b2890dc709c19db9dec0f4/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6", size = 552573, upload-time = "2026-10-04T16:31:32.709Z" }, - { url = "https://files.pythonhosted.org/packages/17/73/6041d20729dffbfdf155c02d65be58bc225a1c1fb548fd87c23ef306138f/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad", size = 615840, upload-time = "2026-10-04T16:31:34.565Z" }, - { url = "https://files.pythonhosted.org/packages/af/9e/418094adaee6b056ce199051b255448ed872829051e341b2294c80da0977/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903", size = 582764, upload-time = "2026-10-04T16:31:36.665Z" }, - { url = "https://files.pythonhosted.org/packages/3d/9b/1698ebf6b840ddfe8b472198abbed6ace35c6e398faeecd6c47dae742a4e/rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b", size = 203795, upload-time = "2026-10-04T16:31:38.53Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037, upload-time = "2026-10-04T16:31:40.468Z" }, -] - -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/70/331d720e6840af08084a81bd22f18a05b79bdd8162703dafd5563a6eb9ac/research_repo_tools-0.1.9.tar.gz", hash = "sha256:7bfb16d7a38e8b3cecd30ce526cf808b377b4480ebd512740198a0361c3d027b", size = 837390, upload-time = "2026-10-14T06:20:36.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, - { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, - { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, - { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c6/af5e359be24f351f8b9603cc565807eebf27e9d06908877736e13eed7fb5/research_repo_tools-0.1.9-py3-none-any.whl", hash = "sha256:c4805004e9a857c072e7b735b033dd13ad256fb283c6e1a4adca0aa59cb67540", size = 369495, upload-time = "2026-10-14T06:20:33.523Z" }, ] [[package]] @@ -1141,59 +182,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/31/234039ddfd4fc10dc16679063d10848423fee6ab13aac3f5d8ce5d1d8b27/rust_just-1.58.0-py3-none-win_amd64.whl", hash = "sha256:145cea724b6d981422ad5357ac6fec53be62b8146ac67a3921c89a0db152887d", size = 2358021, upload-time = "2026-08-04T02:59:19.817Z" }, ] -[[package]] -name = "semantic-version" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, -] - -[[package]] -name = "semgrep" -version = "1.180.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "boltons" }, - { name = "click" }, - { name = "click-option-group" }, - { name = "colorama" }, - { name = "exceptiongroup" }, - { name = "glom" }, - { name = "jsonschema" }, - { name = "mcp" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-requests" }, - { name = "opentelemetry-instrumentation-threading" }, - { name = "opentelemetry-sdk" }, - { name = "packaging" }, - { name = "peewee" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "requests" }, - { name = "rich" }, - { name = "ruamel-yaml" }, - { name = "ruamel-yaml-clib" }, - { name = "semantic-version" }, - { name = "tomli" }, - { name = "typing-extensions" }, - { name = "urllib3" }, - { name = "wcmatch" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/aa/34eac121a4b1859aae125aa8c997bbec1f00b05d3225ba4becff6d3685a1/semgrep-1.180.0.tar.gz", hash = "sha256:14aee785d0bc9656027a33fca7f97e88e089dbc6c7ed3df47f454b0584b0a3d0", size = 518569, upload-time = "2026-10-07T23:22:36.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/94/8ca81606f09f2c33630409f7e1f2ad89c5841f5da062eec8452cee6d51d7/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:f074dcfd6a56095fde32c354af27ef0cce6d15607bef42fa50964dd7042b667d", size = 45810903, upload-time = "2026-10-07T23:24:26.336Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a1/e29003080506e7666ba8225df63632f1ac6417764d4ee991e5dae04f2c0c/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:9c61070eaa7de785a8318e1d00de6c9151efa660cc3e58c3e2ffd6f5e6180af7", size = 49794130, upload-time = "2026-10-07T23:24:30.244Z" }, - { url = "https://files.pythonhosted.org/packages/38/60/66135de6020e792491b0973d95efc89ef729a38c58106394b2798bf58dda/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:cba06f64b10e5ec86a0b3a9757cfdfdf3ce6c2bbdb5e98762aa2a4b0a296f8ca", size = 72563288, upload-time = "2026-10-07T23:24:34.008Z" }, - { url = "https://files.pythonhosted.org/packages/7c/1f/f4af9cc92651f04ab973eac214a62ed23260da9e0696c105b4b9734da453/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:16db2e2706ab814a975eeec25debfad49abba846b7c513d88e041447152f8447", size = 70564254, upload-time = "2026-10-07T23:24:38.551Z" }, - { url = "https://files.pythonhosted.org/packages/37/c5/e712830b05b596fee47c4c8e5382c0439c2ac3aa46272f27818479216f38/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:1bab359e562f0af00217f9f827531c2e1375499b8559c90bed9690007e82565d", size = 79472194, upload-time = "2026-10-07T23:24:43.39Z" }, - { url = "https://files.pythonhosted.org/packages/a1/40/21fefcbf9fe3d9670e98504f71a0974805812bbb308ddee5a31a7c5ba12b/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:d19b447b5d49c435dda099fd5f9d96ec7822e98a8ea6df16630f4f0596ed27ed", size = 77124939, upload-time = "2026-10-07T23:24:47.921Z" }, - { url = "https://files.pythonhosted.org/packages/82/75/3e3a75297e8f181e0a32b5691ee55c78a214d4a72a0370841f9c7674a5da/semgrep-1.180.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:b403865e64f5ba0691630adf9bc13592cc38830726d4a114960d7e2d3fb21fc6", size = 58108036, upload-time = "2026-10-07T23:24:52.232Z" }, -] - [[package]] name = "shellcheck-py" version = "0.11.0.1" @@ -1219,165 +207,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/4a/755b7c7cde56dd7306cea2a50e75a8a6a9281df52e124a5bb0b03474c6ae/shfmt_py-4.2.0-py2.py3-none-win_amd64.whl", hash = "sha256:566f46c036cb475ac84ac952d83122b67c9557418c24b04628c5782f9f57eccf", size = 1662632, upload-time = "2026-09-07T15:25:09.141Z" }, ] -[[package]] -name = "sse-starlette" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/be/0123026f719d1a7936f214a88b553bb5701e04ff2511147c1dab0c5035eb/sse_starlette-3.5.0.tar.gz", hash = "sha256:75de713aa8a9441513cc283220826da079d982770965b951e9437720e8bafdb2", size = 36794, upload-time = "2026-09-28T17:48:14.7Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/e4/cdda14023c316d71493bc54fdffc3dd006631b88866145c9d3cc33e0f1df/sse_starlette-3.5.0-py3-none-any.whl", hash = "sha256:3e6e1070df3f0f5d9cea81496de92dbb72f6721871d99748ece67441dd8b7997", size = 17407, upload-time = "2026-09-28T17:48:13.228Z" }, -] - -[[package]] -name = "starlette" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" }, -] - -[[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/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.86" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/fa/9b35728cc3c04d64f434c656836fa3c5f6e384a7813e48e63245510d5c69/ty-0.0.86.tar.gz", hash = "sha256:6edcaf52e207d653873d5f495c1a8470075b232030714c981f07f0a1075bc343", size = 7911723, upload-time = "2026-10-09T22:20:57.83Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/27/dd422283667ce10d60af57edc3ccab078084282ee304a5c22b5ed79390c6/ty-0.0.86-py3-none-linux_armv6l.whl", hash = "sha256:305c238df5dedf97f1b65e52d2c446a41d11551291e37054858c00c5627bf15b", size = 14159390, upload-time = "2026-10-09T22:20:17.583Z" }, - { url = "https://files.pythonhosted.org/packages/57/e3/838ea547c177983976103d3a54b346afc22d0ff97943ab58c7b7aff1f753/ty-0.0.86-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27a8a7cf26a1cd17d93586eb20aadbcc535fa6ad4b535813ee8fc52560c31efd", size = 13684426, upload-time = "2026-10-09T22:20:19.939Z" }, - { url = "https://files.pythonhosted.org/packages/bc/62/dfde84ffab702545f866350675b16f8df6555d3c41123c25e7a509fe857e/ty-0.0.86-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40e5fc602af41b0ea89dd1d19fe8da7e75322e6e0db95ea23340a858b409ac2b", size = 12845457, upload-time = "2026-10-09T22:20:21.979Z" }, - { url = "https://files.pythonhosted.org/packages/47/b2/7e0e7ffc8b3961f5f74893ba9b98433b922d3f1ae6621d1e3e3e20663e1d/ty-0.0.86-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4b525ce4feb60aa2dd6becae4c470daea889149ba9dc7699ffbcf41448f69a9", size = 13656310, upload-time = "2026-10-09T22:20:24.544Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c8/977594e024d154f0e064b48a6f7e39a0e1e55a524349dba22146f01e02db/ty-0.0.86-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a000ddcc0b99bd1ab8c8277630711061a36ae83395780fdb935bad540ebe7103", size = 13887036, upload-time = "2026-10-09T22:20:26.844Z" }, - { url = "https://files.pythonhosted.org/packages/3a/75/67e8e7bf89a6be7535768380cb20569b460b89cb09d5c82ce27411782786/ty-0.0.86-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3848f3cab206e9798e5a9f7c4ce8a80297b0a88ba9aa8a091360a298e97424", size = 14805103, upload-time = "2026-10-09T22:20:29.274Z" }, - { url = "https://files.pythonhosted.org/packages/a1/14/a7f3b88cc76156feaf5d48601bd5da96a116f846ad0abc526df03a735bcc/ty-0.0.86-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:087bad6ae8fa7248a16c01f5646c121dccaacbaf320ff544eed4e8c5c1fd7555", size = 15203931, upload-time = "2026-10-09T22:20:31.736Z" }, - { url = "https://files.pythonhosted.org/packages/a7/23/42f6ad5d4bb7091d70a0e96d7d436d6bc9a490210a928727dde5d883ed26/ty-0.0.86-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7f2721746c9ee64626c69da9245faedc0f375024bf41458e45eb7b28fe5f63", size = 15094434, upload-time = "2026-10-09T22:20:34.269Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b8/211781d3c8de5fa88437412b7a8c0d7a632596033cff51d5004b08e94e1c/ty-0.0.86-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c18501c54874149d05e5199211146876afeaeee2af234042e56dccc9d5812a", size = 14436812, upload-time = "2026-10-09T22:20:36.795Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/518cc7ef2586384345be675d470e0087591810304b5d994d0a2194c7d68e/ty-0.0.86-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0a48967c8adba6b12035243f225a0585b0bc5f0bf940a4829876bc455ebf3d2f", size = 14671116, upload-time = "2026-10-09T22:20:39.277Z" }, - { url = "https://files.pythonhosted.org/packages/7a/18/a820553ad3206d0cdfad5866dd64939c7e3be73e891d88c73f7fd3acb5d6/ty-0.0.86-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:561a4eae98f6ff05395e8d7be8151fdb328eeaead1ac63dc721ca891623f7924", size = 13586361, upload-time = "2026-10-09T22:20:41.444Z" }, - { url = "https://files.pythonhosted.org/packages/22/58/f1700176e6ae4d0a3d25e447734c60ca464d6e76ed2e8a32920ebd41e523/ty-0.0.86-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccad053ce880a1f8c0850d9dd1f277f3ee559a98bded3f93472268376ceccc2d", size = 13900289, upload-time = "2026-10-09T22:20:43.955Z" }, - { url = "https://files.pythonhosted.org/packages/59/86/2f0e3c9a055f8606911a80311763a089784b66af22dd80fd3290e3b167c6/ty-0.0.86-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7c16d22857bad1da56b147bf53ef0ed2252d7677dc5e7ee51c757b2e52ab6351", size = 14235785, upload-time = "2026-10-09T22:20:46.401Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1d/72fa2347b641143b0440296144a65f424fe047302cb13af059988f1de7a4/ty-0.0.86-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d863f8f190e96878ed1900a8a6d5093f37761c0c47409e2bdb3d9370e37471e1", size = 14559840, upload-time = "2026-10-09T22:20:48.753Z" }, - { url = "https://files.pythonhosted.org/packages/47/4a/a798be4aee4ea0bb427c22f157945d0ac41310f4d6e058b3758dfb859451/ty-0.0.86-py3-none-win32.whl", hash = "sha256:1ac3d8fe9efae04fbf1407583ed4118e0364a45b49a020d62b202846695a2ba1", size = 13133227, upload-time = "2026-10-09T22:20:51.034Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a0/c9d585fa1c5bf4ed2727f576f1542bc13e268f036451a79d057c249fc26f/ty-0.0.86-py3-none-win_amd64.whl", hash = "sha256:6fe4116fb1a7ad3c4f7804d9241dc81187717fbd9f5fed5b50e2752a20e17a67", size = 13665391, upload-time = "2026-10-09T22:20:53.237Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7e/d9811358bce1e54ac1b93bd44f4c45eba5e2e92672c5410248776c54319e/ty-0.0.86-py3-none-win_arm64.whl", hash = "sha256:0df36f494c5d07f68ebec3edb74fc719c4c959db7061c2018f1dff2dfe62150b", size = 13237981, upload-time = "2026-10-09T22:20:55.586Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, -] - -[[package]] -name = "urllib3" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.54.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, -] - -[[package]] -name = "wcmatch" -version = "8.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bracex" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ea/c4/55e0d36da61d7b8b2a49fd273e6b296fd5e8471c72ebbe438635d1af3968/wcmatch-8.5.2.tar.gz", hash = "sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2", size = 114983, upload-time = "2024-05-15T12:51:08.054Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/78/533ef890536e5ba0fd4f7df37482b5800ecaaceae9afc30978a1a7f88ff1/wcmatch-8.5.2-py3-none-any.whl", hash = "sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478", size = 39397, upload-time = "2024-05-15T12:51:06.2Z" }, -] - -[[package]] -name = "wrapt" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +version = "0.0.87" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/15/8a1e8008a4580c3c7bbf2a2e1a6e0001abbd265728426ffe7fc3eab29e98/ty-0.0.87.tar.gz", hash = "sha256:0f844e1fe0725a69753d22b46cb3dce653f3331ae8e332e46b0f22349a244093", size = 7941034, upload-time = "2026-10-13T23:07:08.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/31/6462292e264fc1fcd61bf3179d197e4e7cc21fe95f25660e6b59b6494af3/ty-0.0.87-py3-none-linux_armv6l.whl", hash = "sha256:1b78781402464a152da80b37d4af503912c0f8b882134cdfcdc2dbbd3f914024", size = 14134045, upload-time = "2026-10-13T23:06:18.791Z" }, + { url = "https://files.pythonhosted.org/packages/67/12/6c93cf5f2ce2a00176e8d580d06fc6deb79c13def13c64d833b25860c34f/ty-0.0.87-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7e4a164821ca74733bcaf1a5cca05e0fb7b2bd7b846385b3aa2a042a60d0a0bc", size = 13705845, upload-time = "2026-10-13T23:06:22.031Z" }, + { url = "https://files.pythonhosted.org/packages/f1/03/7cc79acc2a01b3e7146ae8c3df2445c7fe4930e54c3d8b9c052bfa27f389/ty-0.0.87-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d38ebf4da3fb234676236b38e03d24014aa3522af8533c6f0f5eccd4a9de147e", size = 12795984, upload-time = "2026-10-13T23:06:24.969Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/db90201ef1ce9a79e5cefbf7d8faa80eadc2b4f05a7a32ac126b1d33c815/ty-0.0.87-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6ed0a498db9fa8ffb421014698fbca6fa32699d9544156aeeda763b8d8984d0", size = 13659572, upload-time = "2026-10-13T23:06:27.974Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6e/8e59624cbb81358fd6560fe7a2d0994b6531052dd9729e972b8a362325ca/ty-0.0.87-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9a3538293a2aef66fb618cf70ef60f5dca23479dc008ad4ff4b861cff043e78", size = 13860982, upload-time = "2026-10-13T23:06:31.115Z" }, + { url = "https://files.pythonhosted.org/packages/61/ba/6f444935dc40ba16805aedefcd0ee942a682de1fb85c794620233320cbab/ty-0.0.87-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73d540684ec438b5087cbb218898b6030395055698e9132fccc52267e5820522", size = 14834270, upload-time = "2026-10-13T23:06:34.406Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ef/6339f9af05479215a5f351c287e86b2acacd941c918dbe3f702dced32c97/ty-0.0.87-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61c3fded55b32db1a6944f12079c859c7d03da17cd666858ae8acca5a6c15af7", size = 15218871, upload-time = "2026-10-13T23:06:37.224Z" }, + { url = "https://files.pythonhosted.org/packages/9c/98/fa7b9a8dce2248436b13728ad6462f47293c01141ec4afc0184073e6d93f/ty-0.0.87-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75bf154f9f4ea24c3fa2b8d610aee4bfb2ee7ba8f78d9cd763a896a829a87a2a", size = 15134752, upload-time = "2026-10-13T23:06:40.231Z" }, + { url = "https://files.pythonhosted.org/packages/3a/62/dc4d7135c5ea1bf6f5429509113e1c8092ff4e55399f527c508edaba20bb/ty-0.0.87-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8caa3316ea81e794fcd98d1e4d3aaa5fae84790154d88c3e85ac3bd01c537bd", size = 14510734, upload-time = "2026-10-13T23:06:43.029Z" }, + { url = "https://files.pythonhosted.org/packages/35/76/2fd26f136ae967095f222ce75ad58d2c8472da355fc4a6d24662d8c98123/ty-0.0.87-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:930b045a2965f3332d5dbad8ed47076aaeb5c6ead6addf3d4d4e10bc37929901", size = 14681985, upload-time = "2026-10-13T23:06:45.867Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b9/38ea15b4c4c5167f9c93ab5062fe438acff9e7851af615c7580722da2aaa/ty-0.0.87-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:547226807687a240bb2e3c52ef37cdf25b2b33f200dcc659bfa3aac61028b259", size = 13592754, upload-time = "2026-10-13T23:06:48.469Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ce/ca367cee9e4ea5cee7b0b60c37f1d091789e4c784192003ba7695dc18f53/ty-0.0.87-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2791e65f320cd8af1a7b46eed458b09710efb455add9138d4a973352b171ca30", size = 13881344, upload-time = "2026-10-13T23:06:51.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/3b/042a2dd75d83f7bd94afe2c1fd25dbbbbe01ec56c27a4108804068f8e0d2/ty-0.0.87-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4a1eb91eb6de896a3f93847e171d1cf63e3490518bfa3cc69b0e635e9d16d7d0", size = 14259114, upload-time = "2026-10-13T23:06:54.476Z" }, + { url = "https://files.pythonhosted.org/packages/21/99/500885b4ee1f825bde8b3c45ef42dff869d4f30328f34bac90c75065953e/ty-0.0.87-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ed384e2d5e72b14664344e676d091f7ec693ad580d69cd204e01732542e4bf7", size = 14573555, upload-time = "2026-10-13T23:06:57.159Z" }, + { url = "https://files.pythonhosted.org/packages/79/39/09f9274119899ed4e15630a544735c31b32f6bbaeea4c40a788cecf8df3d/ty-0.0.87-py3-none-win32.whl", hash = "sha256:12c85c47d62d3b03c3ef00e1e74a31788aac5de3102d86b687d928d889cfbe83", size = 13137336, upload-time = "2026-10-13T23:06:59.816Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/ecfc716c98bb1eeac9d8743221403c5e7f2f0ed69858af881a0e5b775739/ty-0.0.87-py3-none-win_amd64.whl", hash = "sha256:767219b8fedd2f48875c1ea3557f39734642073a2e039c6ab67ce5a50b65ddf0", size = 13660330, upload-time = "2026-10-13T23:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/94/89/aa0eb234360cb6c8efc6ac45694000dfe3c3e590a30e87f2f9631190d339/ty-0.0.87-py3-none-win_arm64.whl", hash = "sha256:db4aa3a464c43d155ac8a3387a76173183ce4c141a8de1b6cb956e6a5a5e7d52", size = 13217514, upload-time = "2026-10-13T23:07:06.177Z" }, ] [[package]] @@ -1392,12 +244,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/28/a0/8fc2d68e132cf918f wheels = [ { url = "https://files.pythonhosted.org/packages/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, ] - -[[package]] -name = "zipp" -version = "4.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/23/655a1802fe8041302c959774ca7c80b53bc24737ff3ef45cb50ef11bd96c/zipp-4.1.1.tar.gz", hash = "sha256:7ebb7a44c021b29fd8dbd7cce6812d0d7b5b454521f93cc71af6ccd155aaa70b", size = 27649, upload-time = "2026-10-03T17:03:03.452Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/98/df615823cd9419131ce19fba00de53a663794369e198aade064a244b385d/zipp-4.1.1-py3-none-any.whl", hash = "sha256:8979f52d874162f485ff2981e3891f3a3317b7a3dd43ff1e1775b9304f307a9c", size = 10582, upload-time = "2026-10-03T17:03:02.506Z" }, -]