diff --git a/AGENTS.md b/AGENTS.md index 0e38bdf..c8e8df1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,6 +180,9 @@ When user requests commit message generation: - **Unsafe Rust is forbidden.** Keep the manifest-level `unsafe_code = "forbid"` lint and crate/module `#![forbid(unsafe_code)]` enforcement intact. +- **Dead code is forbidden.** Remove unused items instead of suppressing the + lint; never add `#[allow(dead_code)]` or `#![allow(dead_code)]`, including in + tests and static-analysis fixtures. - **ALLOWED**: Run formatters/linters: `cargo fmt`, `cargo clippy`, `cargo doc`, `taplo fmt`, `taplo lint`, `uv run --locked ruff check --fix`, `uv run --locked ruff format`, `rumdl`, `dprint`, `typos`, `actionlint` @@ -207,7 +210,7 @@ When user requests commit message generation: ### Rust -- The current MSRV and pinned contributor/CI toolchain are Rust 1.97.1. Keep +- The current MSRV and pinned contributor/CI toolchain are Rust 1.98.0. Keep `Cargo.toml`, `rust-toolchain.toml`, and `clippy.toml` aligned when that baseline changes deliberately. - Prefer borrowed APIs by default: @@ -292,6 +295,7 @@ just ci # Full CI simulation (checks + tests + examples + bench co just test # Lib + doc tests (fast) just test-all # All tests (Rust, benchmark inputs, and Python) just examples # Run all examples +just update # Update dependency locks and repository-owned Cargo tools ``` ### Detailed Command Reference diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c65679..6fb4a82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,8 +6,8 @@ clarity, and the fixed-dimension stack-allocation model. ## Getting Started -Install Rust 1.97.1 through [rustup](https://rustup.rs/), Git, Python 3.14, -[`uv` 0.12.3](https://docs.astral.sh/uv/), and `jq`. Install the repository's +Install Rust 1.98.0 through [rustup](https://rustup.rs/), Git, Python 3.14, +[`uv` 0.12.5](https://docs.astral.sh/uv/), and `jq`. Install the repository's pinned `just` version from its locked dependency graph: ```bash @@ -25,6 +25,14 @@ just ci # run the comprehensive local CI path Use `just fix` when you intentionally want formatters and automatic fixes to change files. Run `just --list` for the full command surface. +Use `just update` for deliberate dependency and tool maintenance. It composes +`just update-dependencies`, which advances Cargo dependency requirements and +the Cargo/uv locks, with `just update-cargo-tools`, which upgrades only the +Cargo CLI packages owned by `setup-tools` and atomically reconciles their root +`justfile` pins. The tool updater requires `cargo-install-update` from the +`cargo-update` package and does not touch unrelated Cargo executables or uv's +user-global tool environments. + The repository uses `cargo-nextest` for runnable Rust tests, `cargo-machete` for unused-dependency checks, and `just cargo-lock-check` to verify that the committed Cargo lockfile matches the manifest. `rumdl` checks Markdown, @@ -44,25 +52,34 @@ global `RUSTFLAGS='-D warnings'`, this denies lint warnings only for local packages without changing rustc arguments and creating separate cache artifacts. Rustdoc has a separate warning contract, so `doc-check` continues to set `RUSTDOCFLAGS='-D warnings'` for the default and `exact` documentation -surfaces. See the [Rust 1.97 release notes](https://doc.rust-lang.org/stable/releases.html#version-1970-2026-07-09) +surfaces. See the [Rust 1.98 release notes](https://doc.rust-lang.org/stable/releases.html#version-1980-2026-08-20) and [Cargo configuration reference](https://doc.rust-lang.org/cargo/reference/config.html#buildwarnings). -The remaining Rust 1.97 tooling changes require no repository configuration: - -- Rust's default v0 symbol mangling is accepted. Repository code, benchmark - reporting, and support scripts do not parse raw Rust symbols or require the - legacy format. Any future symbol-processing tool must support v0 rather than - forcing the nightly-only legacy override. -- The warn-by-default `linker_messages` lint is covered by the manifest's - warnings-deny policy. The Rust 1.97.1 upgrade completed `just ci` on Linux, - macOS, and Windows without linker diagnostics, so no platform suppression is - justified. -- `resolver.lockfile-path` is not configured. Local, CI, release-benchmark, and - historical-comparison worktrees are writable and use the committed root - `Cargo.lock`; an alternate path would split the lockfile and provenance source - without helping a read-only-source workflow. -- The updated must-use behavior for `Result<_, Infallible>` and - `RepeatN::default` do not intersect production code in this repository. +Rust 1.98's `f32`/`f64::algebraic_{add,sub,mul,div,rem}` operations are +forbidden in `src/`, examples, and benchmark implementations. They permit +real-number transformations such as reassociation with unspecified precision, +which can invalidate the crate's documented error bounds and deterministic +operation order. Their relaxed treatment of non-finite values and signed zero +also conflicts with typed non-finite classification, while varying results +would weaken reproducibility and benchmark comparability. Repository Semgrep +rules enforce this policy; retain ordinary IEEE-754 operations and `mul_add` in +the existing numerical paths. + +Any future approximate or fast-math API requires a separate design issue, an +explicit public contract, independent correctness analysis, and representative +benchmarks. It must remain opt-in and must not change existing APIs. + +The remaining Rust 1.98 changes require no repository configuration: + +- `core::fmt::NumBuffer`, integer `format_into`, `NonZero::from_str_radix`, + circumfix stripping, UTF-16 decoding, and mutable atomic-slice APIs do not + simplify an existing path or address a demonstrated bottleneck. +- The compatibility changes for runtime symbols, trait-object lifetimes, + ambiguous imports, attributes, structural equality, `assert_eq!` temporaries, + and derived ordering require no source change after comprehensive validation. +- New or promoted targets do not alter the Linux, macOS, and Windows MSVC CI + matrix. Cargo 1.98's stable changes are fixes for Windows credential-provider + line endings and diagnostic capitalization; no configuration change is needed. ## Contributor Workflow diff --git a/Cargo.lock b/Cargo.lock index 7151266..a6c8ca7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,13 +85,13 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -108,9 +108,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "shlex", @@ -151,18 +151,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstyle", "clap_lex", @@ -371,9 +371,9 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "fnv" @@ -383,21 +383,21 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-task", @@ -571,9 +571,9 @@ checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" [[package]] name = "glam" -version = "0.33.3" +version = "0.33.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360bd2cd76e0cd9032d42cf2922155cecea2685b0cfa4630c3246df030bcfd6" +checksum = "c426daf70099baff33fac0ba85151c42424861c56828cef9ba02dacb78eb6b26" [[package]] name = "half" @@ -628,9 +628,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -696,7 +696,7 @@ dependencies = [ "glam 0.30.10", "glam 0.31.1", "glam 0.32.1", - "glam 0.33.3", + "glam 0.33.5", "matrixmultiply", "num-complex", "num-rational", @@ -797,9 +797,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -1155,9 +1155,9 @@ dependencies = [ [[package]] name = "safe_arch" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a52ec151f024d703f9fd65abb7cbe81e7cdb39f18917a3a37e3014470dc7c59" +checksum = "42c6efa15875e6ecb39ca61fb0b0c1a40b84fac5a5ffe71eef7d1000c8eb3f5f" dependencies = [ "bytemuck", ] @@ -1228,9 +1228,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simba" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "834a9952211d4a60ff10ac1d20cc01640a75bdc0a0c688d62f359ea7d17459f3" +checksum = "f1a7200d82ff1c7b4235efd12f11e2537a402c91cf83a5cb97ce80eef787fde7" dependencies = [ "approx", "num-complex", @@ -1388,9 +1388,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -1401,9 +1401,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1411,9 +1411,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -1424,18 +1424,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -1505,18 +1505,18 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9505548..bbaa418 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "la-stack" version = "0.4.4" edition = "2024" -rust-version = "1.97.1" +rust-version = "1.98.0" license = "BSD-3-Clause" description = "Fast, stack-allocated linear algebra for fixed dimensions" readme = "README.md" diff --git a/README.md b/README.md index 41ef0fb..b5618d3 100644 --- a/README.md +++ b/README.md @@ -570,8 +570,8 @@ cargo run --features exact --example exact_solve_3x3 A short contributor workflow: -Install Rust 1.97.1 through [rustup](https://rustup.rs/), Git, Python 3.14, -[`uv` 0.12.3](https://docs.astral.sh/uv/), and `jq`. Then install the pinned +Install Rust 1.98.0 through [rustup](https://rustup.rs/), Git, Python 3.14, +[`uv` 0.12.5](https://docs.astral.sh/uv/), and `jq`. Then install the pinned `just` release from its locked dependency graph: ```bash diff --git a/clippy.toml b/clippy.toml index 3b3d5b6..d507f62 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,6 +1,6 @@ # Clippy configuration for the la-stack crate. # Keep lint behavior aligned with Cargo.toml and rust-toolchain.toml. -msrv = "1.97.1" +msrv = "1.98.0" # Lint levels are owned by Cargo.toml. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 8f27353..3c0dcd2 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -14,7 +14,7 @@ to crates.io. ## Conventions and environment -The current MSRV and pinned release-validation toolchain are Rust 1.97.1. Keep +The current MSRV and pinned release-validation toolchain are Rust 1.98.0. Keep `Cargo.toml`, `rust-toolchain.toml`, and `clippy.toml` aligned whenever a future release deliberately changes that baseline. diff --git a/docs/roadmap.md b/docs/roadmap.md index bfd7bb3..6bc3c7f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -146,6 +146,21 @@ The goal is targeted profiling and implementation cleanup for operations where to changes that preserve numerical behavior, allocation-free fixed-size storage, and clear const-generic code. +### v0.4.5 Rust 1.98 Numerical Policy + +The `v0.4.5` milestone continues stable-Rust maintenance without broadening the +crate's scalar or algorithm scope. + +- [#208](https://github.com/acgetchell/la-stack/issues/208) raises the MSRV and + pinned contributor/CI toolchain to Rust 1.98, audits the final stable release, + and adds a repository guard against the new algebraic floating-point + operations in correctness-sensitive source, examples, and benchmarks. + +The existing IEEE 754 operations, deterministic accumulation order, error +bounds, exact fallbacks, and typed non-finite behavior remain authoritative. +Any future approximate or fast-math API requires a separate opt-in design and +correctness analysis. + ### v0.5.0 Generic Const Expressions `v0.5.0` is reserved for the post-stabilization const-generic API revision. diff --git a/justfile b/justfile index ddbdeb1..b15e359 100644 --- a/justfile +++ b/justfile @@ -17,18 +17,19 @@ export PATH := cargo_home + "/bin" + path_separator + env_var("PATH") _coverage_base_args := '''--features exact \ --workspace --lib --tests \ --verbose''' -cargo_llvm_cov_version := "0.8.7" +cargo_edit_version := "0.13.13" +cargo_llvm_cov_version := "0.9.0" cargo_machete_version := "0.9.2" cargo_nextest_version := "0.9.143" clippy_sarif_version := "0.8.0" -dprint_version := "0.55.2" +dprint_version := "0.56.0" git_cliff_version := "2.13.1" just_version := "1.58.0" -rumdl_version := "0.2.53" +rumdl_version := "0.2.58" sarif_fmt_version := "0.8.0" taplo_version := "0.10.0" typos_version := "1.49.0" -uv_version := "0.12.3" +uv_version := "0.12.5" zizmor_version := "1.29.0" # Internal helpers: ensure external tooling is installed @@ -37,6 +38,19 @@ _ensure-actionlint: _ensure-uv set -euo pipefail uv run --locked actionlint -version >/dev/null +_ensure-cargo-edit: + #!/usr/bin/env bash + set -euo pipefail + installed_version="" + if cargo upgrade --version >/dev/null 2>&1; then + installed_version="$(cargo upgrade --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" + fi + if [[ "$installed_version" != "{{ cargo_edit_version }}" ]]; then + echo "❌ 'cargo-edit' {{ cargo_edit_version }} not found. Install with:" + echo " cargo install --locked cargo-edit --version {{ cargo_edit_version }}" + exit 1 + fi + _ensure-cargo-llvm-cov: #!/usr/bin/env bash set -euo pipefail @@ -478,6 +492,7 @@ help-workflows: @echo "Setup:" @echo " just setup # Setup project environment (depends on setup-tools)" @echo " just setup-tools # Install/verify external tooling" + @echo " just update # Update dependencies and repository-owned Cargo tools" @echo "" @echo "Testing:" @echo " just coverage # Generate coverage report (HTML)" @@ -678,7 +693,7 @@ rust-core-check: cargo-lock-check fmt-check clippy-core doc-check semgrep semgre # Repository-owned Semgrep rules for project-specific diagnostics. semgrep: _ensure-uv - uv run --locked semgrep --metrics off --error --strict --timeout 30 --config semgrep.yaml . + uv run --locked 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: _ensure-uv @@ -773,6 +788,11 @@ setup-tools: cargo install --locked just --version "$just_version" fi + cargo_edit_version="{{ cargo_edit_version }}" + if ! cargo upgrade --version >/dev/null 2>&1 || [[ "$(cargo upgrade --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$cargo_edit_version" ]]; then + cargo install --locked cargo-edit --version "$cargo_edit_version" + fi + cargo_llvm_cov_version="{{ cargo_llvm_cov_version }}" if ! have cargo-llvm-cov || [[ "$(cargo llvm-cov --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$cargo_llvm_cov_version" ]]; then cargo install --locked cargo-llvm-cov --version "$cargo_llvm_cov_version" @@ -821,6 +841,7 @@ setup-tools: have jq || { echo "❌ 'jq' is still missing."; exit 1; } echo " ✓ jq" verify_tool_version just "$just_version" + verify_tool_version cargo-upgrade "$cargo_edit_version" verify_tool_version cargo-llvm-cov "$cargo_llvm_cov_version" verify_tool_version cargo-machete "$cargo_machete_version" verify_tool_version cargo-nextest "$cargo_nextest_version" @@ -1020,6 +1041,46 @@ toml-parse-check: python-sync unused-deps: _ensure-cargo-machete cargo machete +# Update dependency requirements, locks, and locally installed Cargo tools owned by this repository. +update: update-dependencies update-cargo-tools + @echo "✅ Repository dependencies and tools updated." + +# Update locally installed Cargo CLI tools owned by `setup-tools` and reconcile their pins. +[doc('Update Cargo CLI tools owned by setup-tools and reconcile their root justfile pins.')] +update-cargo-tools: _ensure-uv + #!/usr/bin/env bash + set -euo pipefail + + if ! command -v cargo-install-update >/dev/null 2>&1; then + echo "❌ 'cargo-install-update' not found. Install it with:" + echo " cargo install --locked cargo-update" + exit 1 + fi + + packages=( + cargo-edit + cargo-llvm-cov + cargo-machete + cargo-nextest + dprint + git-cliff + just + rumdl + taplo-cli + typos-cli + zizmor + ) + cargo install-update --locked "${packages[@]}" + uv run --locked update-cargo-tool-pins + +# Advance Cargo dependency declarations, update Cargo and uv locks, then sync uv dev tools. +[doc('Update Cargo.toml dependency requirements and all Cargo/uv locked dependencies.')] +update-dependencies: _ensure-uv _ensure-cargo-edit + cargo upgrade + cargo update + uv lock --upgrade + uv sync --locked --group dev + validate-json: _ensure-jq #!/usr/bin/env bash set -euo pipefail diff --git a/pyproject.toml b/pyproject.toml index ccbe05a..492bcee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,11 +41,12 @@ criterion-dim-plot = "criterion_dim_plot:main" postprocess-changelog = "postprocess_changelog:main" tag-release = "tag_release:main" check-docs-version-sync = "check_docs_version_sync:main" +update-cargo-tool-pins = "update_cargo_tool_pins:main" # Configure setuptools to find modules in scripts/ directory. [tool.setuptools] package-dir = { "" = "scripts" } -py-modules = [ "archive_changelog", "archive_performance", "bench_compare", "check_docs_version_sync", "check_semgrep_fixtures", "criterion_dim_plot", "performance_artifacts", "postprocess_changelog", "subprocess_utils", "tag_release" ] +py-modules = [ "archive_changelog", "archive_performance", "bench_compare", "check_docs_version_sync", "check_semgrep_fixtures", "criterion_dim_plot", "performance_artifacts", "postprocess_changelog", "subprocess_utils", "tag_release", "update_cargo_tool_pins" ] [tool.ruff] line-length = 160 @@ -95,6 +96,7 @@ known-first-party = [ "postprocess_changelog", "subprocess_utils", "tag_release", + "update_cargo_tool_pins", ] force-single-line = false split-on-trailing-comma = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ec4f6a3..a260e23 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] # Pin to MSRV as specified in Cargo.toml -channel = "1.97.1" +channel = "1.98.0" # Essential repository components. Keep checkout and CI setup lean; workflows # install additional targets or components when they need them. diff --git a/scripts/README.md b/scripts/README.md index fd1f4af..9f3096d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -24,6 +24,13 @@ uv sync --locked --group dev - 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 maintenance workflow. It updates Cargo and +uv dependency declarations and locks, upgrades only the Cargo CLI packages +owned by `setup-tools`, and then uses `update-cargo-tool-pins` to reconcile the +installed package versions with the root `justfile` atomically. + ## How to use it ### Comparing performance @@ -253,5 +260,6 @@ validates SemVer, and handles GitHub's 125KB tag-annotation size limit. | `tag_release.py` | Create annotated git tags from CHANGELOG.md sections | | `postprocess_changelog.py` | Normalize and reflow generated git-cliff Markdown safely | | `subprocess_utils.py` | Safe subprocess wrappers for git commands | +| `update_cargo_tool_pins.py` | Reconcile repository-owned Cargo tool pins with installed versions | See `docs/RELEASING.md` for the full release workflow. diff --git a/scripts/archive_changelog.py b/scripts/archive_changelog.py index 969dd17..a3476ff 100755 --- a/scripts/archive_changelog.py +++ b/scripts/archive_changelog.py @@ -287,33 +287,53 @@ def _build_archive_text( def _stage_text(path: Path, text: str) -> Path: path.parent.mkdir(parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile( - "w", - encoding="utf-8", - dir=path.parent, - prefix=f".{path.name}.", - suffix=".tmp", - delete=False, - ) as handle: - handle.write(text) - handle.flush() - os.fsync(handle.fileno()) - return Path(handle.name) + staged: Path | None = None + try: + with tempfile.NamedTemporaryFile( + "w", + encoding="utf-8", + dir=path.parent, + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as handle: + staged = Path(handle.name) + handle.write(text) + handle.flush() + os.fsync(handle.fileno()) + except BaseException: + if staged is not None: + staged.unlink(missing_ok=True) + raise + if staged is None: + msg = "temporary changelog staging completed without a path" + raise AssertionError(msg) + return staged def _stage_bytes(path: Path, payload: bytes) -> Path: path.parent.mkdir(parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile( - "wb", - dir=path.parent, - prefix=f".{path.name}.", - suffix=".tmp", - delete=False, - ) as handle: - handle.write(payload) - handle.flush() - os.fsync(handle.fileno()) - return Path(handle.name) + staged: Path | None = None + try: + with tempfile.NamedTemporaryFile( + "wb", + dir=path.parent, + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as handle: + staged = Path(handle.name) + handle.write(payload) + handle.flush() + os.fsync(handle.fileno()) + except BaseException: + if staged is not None: + staged.unlink(missing_ok=True) + raise + if staged is None: + msg = "temporary changelog restoration completed without a path" + raise AssertionError(msg) + return staged def _replace_path(source: Path, destination: Path) -> None: @@ -336,9 +356,11 @@ def _publish_texts(payloads: dict[Path, str]) -> None: if not payloads: return previous = {path: path.read_bytes() if path.is_file() else None for path in payloads} - staged = {path: _stage_text(path, text) for path, text in payloads.items()} + staged: dict[Path, Path] = {} replaced: list[Path] = [] try: + for path, text in payloads.items(): + staged[path] = _stage_text(path, text) for path, staged_path in staged.items(): _replace_path(staged_path, path) replaced.append(path) diff --git a/scripts/archive_performance.py b/scripts/archive_performance.py index 5af3f22..a7ddf79 100644 --- a/scripts/archive_performance.py +++ b/scripts/archive_performance.py @@ -434,21 +434,23 @@ def _restore_file(path: Path, payload: bytes | None) -> None: path.unlink(missing_ok=True) return path.parent.mkdir(parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile( - "wb", - dir=path.parent, - prefix=f".{path.name}.", - suffix=".restore", - delete=False, - ) as tmp: - restore_path = Path(tmp.name) - tmp.write(payload) - tmp.flush() - os.fsync(tmp.fileno()) + restore_path: Path | None = None try: + with tempfile.NamedTemporaryFile( + "wb", + dir=path.parent, + prefix=f".{path.name}.", + suffix=".restore", + delete=False, + ) as tmp: + restore_path = Path(tmp.name) + tmp.write(payload) + tmp.flush() + os.fsync(tmp.fileno()) restore_path.replace(path) finally: - restore_path.unlink(missing_ok=True) + if restore_path is not None: + restore_path.unlink(missing_ok=True) def _snapshot_regular_file(path: Path, *, label: str) -> bytes | None: diff --git a/scripts/bench_compare.py b/scripts/bench_compare.py index b8a88b2..d399807 100644 --- a/scripts/bench_compare.py +++ b/scripts/bench_compare.py @@ -2130,10 +2130,10 @@ def _write_text_atomic(path: Path, text: str) -> None: suffix=".tmp", delete=False, ) as handle: + staged = Path(handle.name) handle.write(text) handle.flush() os.fsync(handle.fileno()) - staged = Path(handle.name) staged.replace(path) finally: if staged is not None: diff --git a/scripts/performance_artifacts.py b/scripts/performance_artifacts.py index 6772a3b..0b10767 100644 --- a/scripts/performance_artifacts.py +++ b/scripts/performance_artifacts.py @@ -791,11 +791,21 @@ def load_bundle(paths: ArtifactPaths) -> PerformanceBundle: def _stage_payload(path: Path, payload: bytes) -> Path: path.parent.mkdir(parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile("wb", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", delete=False) as tmp: - tmp.write(payload) - tmp.flush() - os.fsync(tmp.fileno()) - return Path(tmp.name) + staged: Path | None = None + try: + with tempfile.NamedTemporaryFile("wb", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", delete=False) as tmp: + staged = Path(tmp.name) + tmp.write(payload) + tmp.flush() + os.fsync(tmp.fileno()) + except BaseException: + if staged is not None: + staged.unlink(missing_ok=True) + raise + if staged is None: + msg = "temporary artifact staging completed without a path" + raise AssertionError(msg) + return staged def _atomic_restore(path: Path, payload: bytes | None) -> None: @@ -810,26 +820,30 @@ def _atomic_restore(path: Path, payload: bytes | None) -> None: def _publish_payloads(paths: ArtifactPaths, csv_payload: bytes, provenance_payload: bytes) -> None: - staged_csv = _stage_payload(paths.csv, csv_payload) - staged_provenance = _stage_payload(paths.provenance, provenance_payload) - previous_csv = paths.csv.read_bytes() if paths.csv.is_file() else None - previous_provenance = paths.provenance.read_bytes() if paths.provenance.is_file() else None + staged_paths: list[Path] = [] try: - _replace_path(staged_csv, paths.csv) - _replace_path(staged_provenance, paths.provenance) - load_bundle(paths) - except BaseException as publication_error: - rollback_errors = _restore_artifact_pair(paths, previous_csv, previous_provenance) - if rollback_errors: - group_message = "release-performance artifact publication and rollback failed" - raise BaseExceptionGroup( - group_message, - [publication_error, *rollback_errors], - ) from None - raise + staged_csv = _stage_payload(paths.csv, csv_payload) + staged_paths.append(staged_csv) + staged_provenance = _stage_payload(paths.provenance, provenance_payload) + staged_paths.append(staged_provenance) + previous_csv = paths.csv.read_bytes() if paths.csv.is_file() else None + previous_provenance = paths.provenance.read_bytes() if paths.provenance.is_file() else None + try: + _replace_path(staged_csv, paths.csv) + _replace_path(staged_provenance, paths.provenance) + load_bundle(paths) + except BaseException as publication_error: + rollback_errors = _restore_artifact_pair(paths, previous_csv, previous_provenance) + if rollback_errors: + group_message = "release-performance artifact publication and rollback failed" + raise BaseExceptionGroup( + group_message, + [publication_error, *rollback_errors], + ) from None + raise finally: - staged_csv.unlink(missing_ok=True) - staged_provenance.unlink(missing_ok=True) + for staged in staged_paths: + staged.unlink(missing_ok=True) def _replace_path(source: Path, destination: Path) -> None: diff --git a/scripts/tests/test_archive_changelog.py b/scripts/tests/test_archive_changelog.py index eecf95c..b8e8ef4 100644 --- a/scripts/tests/test_archive_changelog.py +++ b/scripts/tests/test_archive_changelog.py @@ -5,6 +5,7 @@ import pytest +import archive_changelog as archive_changelog_module from archive_changelog import ( _extract_link_defs, _format_link_defs, @@ -379,6 +380,71 @@ def fail_root_publication(source: Path, destination: Path) -> None: assert existing_archive.read_bytes() == original_archive assert not (archive_dir / "0.2.md").exists() + def test_stage_text_removes_temporary_file_when_fsync_fails( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + target = tmp_path / "CHANGELOG.md" + + def fail_fsync(_descriptor: int) -> None: + msg = "simulated fsync failure" + raise OSError(msg) + + monkeypatch.setattr(archive_changelog_module.os, "fsync", fail_fsync) + + with pytest.raises(OSError, match="simulated fsync failure"): + archive_changelog_module._stage_text(target, "payload\n") + + assert not list(tmp_path.glob(".CHANGELOG.md.*.tmp")) + + def test_stage_bytes_removes_temporary_file_when_fsync_fails( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + target = tmp_path / "0.4.md" + + def fail_fsync(_descriptor: int) -> None: + msg = "simulated fsync failure" + raise OSError(msg) + + monkeypatch.setattr(archive_changelog_module.os, "fsync", fail_fsync) + + with pytest.raises(OSError, match="simulated fsync failure"): + archive_changelog_module._stage_bytes(target, b"payload\n") + + assert not list(tmp_path.glob(".0.4.md.*.tmp")) + + def test_partial_staging_failure_removes_prior_temporary_files( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + first = tmp_path / "first.md" + second = tmp_path / "second.md" + real_stage_text = archive_changelog_module._stage_text + + def fail_second_stage(path: Path, text: str) -> Path: + if path == second: + msg = "simulated second staging failure" + raise OSError(msg) + return real_stage_text(path, text) + + monkeypatch.setattr(archive_changelog_module, "_stage_text", fail_second_stage) + + with pytest.raises(OSError, match="simulated second staging failure"): + archive_changelog_module._publish_texts( + { + first: "first\n", + second: "second\n", + } + ) + + assert not first.exists() + assert not second.exists() + assert not list(tmp_path.glob(".*.tmp")) + def test_idempotent(self, tmp_path: Path) -> None: """Running archive twice produces the same output.""" changelog = tmp_path / "CHANGELOG.md" diff --git a/scripts/tests/test_archive_performance.py b/scripts/tests/test_archive_performance.py index 7dc9551..294bca0 100644 --- a/scripts/tests/test_archive_performance.py +++ b/scripts/tests/test_archive_performance.py @@ -2064,6 +2064,27 @@ def fail_replace(src: Path, dst: Path) -> None: assert not list(current.parent.glob(".PERFORMANCE.md.*.tmp")) +def test_restore_file_removes_temp_when_fsync_fails( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + target = tmp_path / "docs" / "PERFORMANCE.md" + target.parent.mkdir() + target.write_text("current\n", encoding="utf-8") + + def fail_fsync(_descriptor: int) -> None: + msg = "simulated fsync failure" + raise OSError(msg) + + monkeypatch.setattr(archive_performance.os, "fsync", fail_fsync) + + with pytest.raises(OSError, match="simulated fsync failure"): + archive_performance._restore_file(target, b"restored\n") + + assert target.read_text(encoding="utf-8") == "current\n" + assert not list(target.parent.glob(".PERFORMANCE.md.*.restore")) + + def test_failed_archive_index_update_rolls_back_report_and_new_archive( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/scripts/tests/test_bench_compare.py b/scripts/tests/test_bench_compare.py index 761a44e..ee169db 100644 --- a/scripts/tests/test_bench_compare.py +++ b/scripts/tests/test_bench_compare.py @@ -1229,6 +1229,26 @@ def fail_markdown(_path: Path, _text: str) -> None: assert output.read_text(encoding="utf-8") == "old markdown\n" +def test_atomic_markdown_write_removes_temp_when_fsync_fails( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + output = tmp_path / "performance.md" + output.write_text("old\n", encoding="utf-8") + + def fail_fsync(_descriptor: int) -> None: + msg = "simulated fsync failure" + raise OSError(msg) + + monkeypatch.setattr(bench_compare.os, "fsync", fail_fsync) + + with pytest.raises(OSError, match="simulated fsync failure"): + bench_compare._write_text_atomic(output, "new\n") + + assert output.read_text(encoding="utf-8") == "old\n" + assert not list(tmp_path.glob(".performance.md.*.tmp")) + + def test_main_v043_comparison_allows_only_unavailable_balanced_baselines( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/scripts/tests/test_justfile_discoverability.py b/scripts/tests/test_justfile_discoverability.py index 5036d8d..50061e4 100644 --- a/scripts/tests/test_justfile_discoverability.py +++ b/scripts/tests/test_justfile_discoverability.py @@ -2,12 +2,15 @@ import json import os +import re import shutil import stat import subprocess from pathlib import Path from typing import Any +import update_cargo_tool_pins + REPO_ROOT = Path(__file__).resolve().parents[2] @@ -68,3 +71,40 @@ def test_uv_guard_reports_expected_and_actual_versions(tmp_path: Path) -> None: assert result.returncode != 0 assert f"version '9.9.9', expected '{expected}'" in result.stderr + + +def test_update_workflow_composes_scoped_dependency_and_tool_updates() -> None: + """Update recipes should cover repo state without touching unrelated global tools.""" + recipes = just_recipes() + update_dependencies = {dependency["recipe"] for dependency in recipes["update"]["dependencies"]} + + assert update_dependencies == {"update-cargo-tools", "update-dependencies"} + + dependency_result = run_just("--dry-run", "update-dependencies") + dependency_update = dependency_result.stdout + dependency_result.stderr + assert "cargo upgrade" in dependency_update + assert "cargo upgrade --incompatible allow" not in dependency_update + assert "cargo update" in dependency_update + assert "uv lock --upgrade" in dependency_update + assert "uv sync --locked --group dev" in dependency_update + assert "cargo install-update --all" not in dependency_update + assert "uv tool upgrade" not in dependency_update + + tool_result = run_just("--dry-run", "update-cargo-tools") + tool_update = tool_result.stdout + tool_result.stderr + assert "cargo install-update --locked" in tool_update + assert "update-cargo-tool-pins" in tool_update + assert "cargo install-update --all" not in tool_update + assert "uv tool upgrade" not in tool_update + package_block = re.search(r"packages=\(\n(?P.*?)\n\)", tool_update, re.DOTALL) + assert package_block is not None + updated_packages = set(re.findall(r"^\s+([a-z0-9-]+)$", package_block.group("packages"), re.MULTILINE)) + assert updated_packages == set(update_cargo_tool_pins.PIN_TO_PACKAGE.values()) + + +def test_managed_cargo_tool_pins_exist_once_in_root_justfile() -> None: + """Every managed Cargo package should map to one real root Just pin.""" + justfile_text = (REPO_ROOT / "justfile").read_text(encoding="utf-8") + + for pin in update_cargo_tool_pins.PIN_TO_PACKAGE: + assert len(re.findall(rf'(?m)^{re.escape(pin)}\s*:=\s*"[^"]+"\s*$', justfile_text)) == 1 diff --git a/scripts/tests/test_performance_artifacts.py b/scripts/tests/test_performance_artifacts.py index 3821130..2622dd9 100644 --- a/scripts/tests/test_performance_artifacts.py +++ b/scripts/tests/test_performance_artifacts.py @@ -393,6 +393,50 @@ def test_artifact_loader_fails_closed_on_partial_pair(tmp_path: Path) -> None: load_bundle(paths) +def test_stage_payload_removes_temporary_file_when_fsync_fails( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + target = tmp_path / "performance.csv" + + def fail_fsync(_descriptor: int) -> None: + msg = "simulated fsync failure" + raise OSError(msg) + + monkeypatch.setattr(performance_artifacts.os, "fsync", fail_fsync) + + with pytest.raises(OSError, match="simulated fsync failure"): + performance_artifacts._stage_payload(target, b"payload") + + assert not list(tmp_path.glob(".performance.csv.*.tmp")) + + +def test_failed_second_stage_removes_first_temporary_file( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + paths = ArtifactPaths( + csv=tmp_path / "performance.csv", + provenance=tmp_path / "performance.provenance.json", + ) + real_stage_payload = performance_artifacts._stage_payload + + def fail_provenance_stage(path: Path, payload: bytes) -> Path: + if path == paths.provenance: + msg = "simulated provenance staging failure" + raise OSError(msg) + return real_stage_payload(path, payload) + + monkeypatch.setattr(performance_artifacts, "_stage_payload", fail_provenance_stage) + + with pytest.raises(OSError, match="simulated provenance staging failure"): + write_bundle(paths, _bundle()) + + assert not list(tmp_path.glob(".performance.csv.*.tmp")) + assert not paths.csv.exists() + assert not paths.provenance.exists() + + def test_failed_second_replace_restores_prior_valid_pair(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: paths = ArtifactPaths( csv=tmp_path / "performance.csv", diff --git a/scripts/tests/test_update_cargo_tool_pins.py b/scripts/tests/test_update_cargo_tool_pins.py new file mode 100644 index 0000000..9e3546c --- /dev/null +++ b/scripts/tests/test_update_cargo_tool_pins.py @@ -0,0 +1,89 @@ +"""Tests for atomic repository Cargo-tool pin reconciliation.""" + +from typing import TYPE_CHECKING, Never + +import pytest + +import update_cargo_tool_pins +from subprocess_utils import ExecutableNotFoundError + +if TYPE_CHECKING: + from pathlib import Path + + +def installed_output(*, override: tuple[str, str] | None = None) -> str: + """Return representative ``cargo install --list`` output for every managed tool.""" + versions = dict.fromkeys(update_cargo_tool_pins.PIN_TO_PACKAGE.values(), "1.2.3") + if override is not None: + versions[override[0]] = override[1] + return "".join(f"{package} v{version}:\n {package}\n" for package, version in versions.items()) + + +def justfile_text(version: str = "1.2.3") -> str: + """Return one assignment for every managed Just pin.""" + return "".join(f'{pin} := "{version}"\n' for pin in update_cargo_tool_pins.PIN_TO_PACKAGE) + + +def test_reconcile_pins_updates_one_changed_version_atomically(tmp_path: Path) -> None: + justfile = tmp_path / "justfile" + justfile.write_text(justfile_text(), encoding="utf-8") + + changes = update_cargo_tool_pins.reconcile_pins(justfile, installed_output(override=("rumdl", "2.0.0"))) + + assert changes == {"rumdl_version": ("1.2.3", "2.0.0")} + assert 'rumdl_version := "2.0.0"' in justfile.read_text(encoding="utf-8") + assert list(tmp_path.glob(".justfile.*")) == [] + + +def test_reconcile_pins_rejects_missing_package_without_writing(tmp_path: Path) -> None: + justfile = tmp_path / "justfile" + original = justfile_text() + justfile.write_text(original, encoding="utf-8") + incomplete = installed_output().replace("rumdl v1.2.3:\n rumdl\n", "") + + with pytest.raises(ValueError, match="managed Cargo package is not installed: rumdl"): + update_cargo_tool_pins.reconcile_pins(justfile, incomplete) + + assert justfile.read_text(encoding="utf-8") == original + + +def test_update_pin_text_rejects_duplicate_assignment() -> None: + duplicated = justfile_text() + 'rumdl_version := "1.2.3"\n' + + with pytest.raises(ValueError, match="expected exactly one rumdl_version assignment, found 2"): + update_cargo_tool_pins.update_pin_text(duplicated, update_cargo_tool_pins.parse_installed_packages(installed_output())) + + +def test_parse_installed_packages_accepts_prerelease_with_build_metadata() -> None: + version = "1.2.3-rc.1+build.5" + + installed = update_cargo_tool_pins.parse_installed_packages(installed_output(override=("rumdl", version))) + + assert installed["rumdl"] == version + + +def test_reconcile_pins_preserves_prerelease_with_build_metadata(tmp_path: Path) -> None: + version = "1.2.3-rc.1+build.5" + justfile = tmp_path / "justfile" + justfile.write_text(justfile_text(), encoding="utf-8") + + changes = update_cargo_tool_pins.reconcile_pins(justfile, installed_output(override=("rumdl", version))) + + assert changes == {"rumdl_version": ("1.2.3", version)} + assert f'rumdl_version := "{version}"' in justfile.read_text(encoding="utf-8").splitlines() + + +def test_main_reports_missing_cargo_without_traceback( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + def missing_cargo(_args: list[str], **_kwargs: object) -> Never: + msg = "Required executable 'cargo' not found in PATH" + raise ExecutableNotFoundError(msg) + + monkeypatch.setattr(update_cargo_tool_pins, "run_cargo_command", missing_cargo) + + assert update_cargo_tool_pins.main([]) == 1 + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err == "failed to update Cargo-tool pins: Required executable 'cargo' not found in PATH\n" diff --git a/scripts/update_cargo_tool_pins.py b/scripts/update_cargo_tool_pins.py new file mode 100644 index 0000000..cc1774b --- /dev/null +++ b/scripts/update_cargo_tool_pins.py @@ -0,0 +1,114 @@ +"""Reconcile repository Cargo-tool pins with installed package versions.""" + +import argparse +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +from subprocess_utils import ExecutableNotFoundError, run_cargo_command + +PIN_TO_PACKAGE = { + "cargo_edit_version": "cargo-edit", + "cargo_llvm_cov_version": "cargo-llvm-cov", + "cargo_machete_version": "cargo-machete", + "cargo_nextest_version": "cargo-nextest", + "dprint_version": "dprint", + "git_cliff_version": "git-cliff", + "just_version": "just", + "rumdl_version": "rumdl", + "taplo_version": "taplo-cli", + "typos_version": "typos-cli", + "zizmor_version": "zizmor", +} +PACKAGE_HEADER = re.compile(r"^(?P[A-Za-z0-9_-]+) v(?P[^\s:]+):$", re.MULTILINE) +VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$") + + +def parse_installed_packages(output: str) -> dict[str, str]: + """Parse package versions from ``cargo install --list`` output.""" + packages: dict[str, str] = {} + for match in PACKAGE_HEADER.finditer(output): + package = match.group("package") + version = match.group("version") + if package in packages: + msg = f"duplicate installed Cargo package: {package}" + raise ValueError(msg) + if VERSION.fullmatch(version) is None: + msg = f"invalid installed version for {package}: {version}" + raise ValueError(msg) + packages[package] = version + return packages + + +def update_pin_text(text: str, installed: dict[str, str]) -> tuple[str, dict[str, tuple[str, str]]]: + """Return Just source with every managed pin reconciled exactly once.""" + updated = text + changes: dict[str, tuple[str, str]] = {} + for pin, package in PIN_TO_PACKAGE.items(): + version = installed.get(package) + if version is None: + msg = f"managed Cargo package is not installed: {package}" + raise ValueError(msg) + assignment = re.compile(rf'^(?P{re.escape(pin)}\s*:=\s*")(?P[^"]+)(?P"\s*)$', re.MULTILINE) + matches = list(assignment.finditer(updated)) + if len(matches) != 1: + msg = f"expected exactly one {pin} assignment, found {len(matches)}" + raise ValueError(msg) + old_version = matches[0].group("version") + if old_version == version: + continue + updated = assignment.sub(rf"\g{version}\g", updated, count=1) + changes[pin] = (old_version, version) + return updated, changes + + +def reconcile_pins(justfile: Path, installed_output: str) -> dict[str, tuple[str, str]]: + """Atomically reconcile ``justfile`` and return changed pin versions.""" + original = justfile.read_text(encoding="utf-8") + updated, changes = update_pin_text(original, parse_installed_packages(installed_output)) + if not changes: + return changes + + descriptor, temporary_name = tempfile.mkstemp(prefix=f".{justfile.name}.", dir=justfile.parent, text=True) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as stream: + stream.write(updated) + temporary.chmod(justfile.stat().st_mode) + temporary.replace(justfile) + except BaseException: + temporary.unlink(missing_ok=True) + raise + return changes + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--justfile", type=Path, default=Path("justfile"), help="Just source containing repository tool pins") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Reconcile managed Cargo-tool pins from the local Cargo installation.""" + args = parse_args(argv) + try: + result = run_cargo_command(["install", "--list"], timeout=30) + changes = reconcile_pins(args.justfile, result.stdout) + except (ExecutableNotFoundError, OSError, subprocess.SubprocessError, ValueError) as error: + print(f"failed to update Cargo-tool pins: {error}", file=sys.stderr) + return 1 + + if not changes: + print("Cargo-tool pins already match installed repository tools.") + return 0 + for pin, (old_version, new_version) in changes.items(): + print(f"Updated {pin}: {old_version} -> {new_version}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/semgrep.yaml b/semgrep.yaml index 84ad747..aef8835 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -83,6 +83,33 @@ rules: - pattern: $VALUE.unwrap_or_else(|| std::f64::INFINITY) - pattern: $VALUE.unwrap_or_else(|| std::f64::NEG_INFINITY) + - id: la-stack.rust.no-algebraic-float-operations + languages: + - rust + severity: WARNING + message: "Use the crate's defined IEEE-754 operation order; algebraic float operations are forbidden." + metadata: + category: correctness + rationale: >- + Algebraic float operations permit real-number transformations with + unspecified precision. Reassociation or relaxed NaN and signed-zero + behavior can invalidate error bounds, reproducibility, non-finite + classification, exact-fallback filters, and benchmark comparisons. + paths: + include: + - "/src/**/*.rs" + - "/examples/**/*.rs" + - "/benches/**/*.rs" + # Test-only violation fixture: `just semgrep` excludes it, while + # `just semgrep-test` scans it directly. + - "/tests/semgrep/src/project_rules/algebraic_float.rs" + pattern-either: + - pattern: $VALUE.algebraic_add(...) + - pattern: $VALUE.algebraic_sub(...) + - pattern: $VALUE.algebraic_mul(...) + - pattern: $VALUE.algebraic_div(...) + - pattern: $VALUE.algebraic_rem(...) + - id: la-stack.rust.no-public-infallible-raw-f64-constructors languages: - rust diff --git a/tests/semgrep/src/project_rules/algebraic_float.rs b/tests/semgrep/src/project_rules/algebraic_float.rs new file mode 100644 index 0000000..5fec58b --- /dev/null +++ b/tests/semgrep/src/project_rules/algebraic_float.rs @@ -0,0 +1,41 @@ +pub fn forbidden_f64_operations(left: f64, right: f64) -> [f64; 5] { + [ + // ruleid: la-stack.rust.no-algebraic-float-operations + left.algebraic_add(right), + // ruleid: la-stack.rust.no-algebraic-float-operations + left.algebraic_sub(right), + // ruleid: la-stack.rust.no-algebraic-float-operations + left.algebraic_mul(right), + // ruleid: la-stack.rust.no-algebraic-float-operations + left.algebraic_div(right), + // ruleid: la-stack.rust.no-algebraic-float-operations + left.algebraic_rem(right), + ] +} + +pub fn forbidden_f32_associated_operation(left: f32, right: f32) -> f32 { + // ruleid: la-stack.rust.no-algebraic-float-operations + f32::algebraic_add(left, right) +} + +pub fn forbidden_f64_associated_operation(left: f64, right: f64) -> f64 { + // ruleid: la-stack.rust.no-algebraic-float-operations + f64::algebraic_add(left, right) +} + +pub fn permitted_f64_operations(left: f64, right: f64) -> [f64; 6] { + [ + // ok: la-stack.rust.no-algebraic-float-operations + left + right, + // ok: la-stack.rust.no-algebraic-float-operations + left - right, + // ok: la-stack.rust.no-algebraic-float-operations + left * right, + // ok: la-stack.rust.no-algebraic-float-operations + left / right, + // ok: la-stack.rust.no-algebraic-float-operations + left % right, + // ok: la-stack.rust.no-algebraic-float-operations + left.mul_add(right, 1.0), + ] +} diff --git a/tests/semgrep/src/project_rules/portable_policy.rs b/tests/semgrep/src/project_rules/portable_policy.rs index 9219ee6..9504758 100644 --- a/tests/semgrep/src/project_rules/portable_policy.rs +++ b/tests/semgrep/src/project_rules/portable_policy.rs @@ -1,5 +1,3 @@ -#![allow(dead_code, unused_imports)] - use num_traits::NumCast; pub fn stdio_diagnostic_fixture() { diff --git a/uv.lock b/uv.lock index 639293b..6b22d5b 100644 --- a/uv.lock +++ b/uv.lock @@ -126,37 +126,96 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, - { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, - { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, - { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, - { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, - { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, - { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, - { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, - { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, - { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, - { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, - { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] @@ -279,14 +338,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.75.0" +version = "1.75.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/73/74bcab964c9a7a61f2bb71e8179b0f13e6fa98f7ce00fd168aab291e4a2e/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071", size = 150967, upload-time = "2026-08-06T06:24:51.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79", size = 300626, upload-time = "2026-08-06T06:23:46.696Z" }, ] [[package]] @@ -337,11 +396,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -722,25 +781,25 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.2" +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/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +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/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, + { 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.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +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/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, + { 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]] @@ -775,11 +834,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, ] [[package]] @@ -1066,27 +1125,27 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.4.6" +version = "3.4.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/10/a34c656829ffc1c4b22ef36d70d9ebb6b99c020e2aeb17cee5485099f028/sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627", size = 32542, upload-time = "2026-07-20T14:16:32.201Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/36/e10c1d1b7ca881d2625db2ec28508578499187bb1c389952c398474e1834/sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6", size = 16516, upload-time = "2026-07-20T14:16:30.978Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, ] [[package]] name = "starlette" -version = "1.3.1" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, ] [[package]] @@ -1152,14 +1211,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +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/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { 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]] @@ -1173,15 +1232,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.1" +version = "0.52.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571, upload-time = "2026-08-01T18:19:30.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, ] [[package]]