diff --git a/.gitattributes b/.gitattributes index c50bb1f3..ea16e11b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,13 @@ uv.lock text eol=lf requirements.lock text eol=lf docker/locks/*.lock text eol=lf +# constraints.lock was MISSING here (`git check-attr text -- constraints.lock` -> unspecified), so under +# core.autocrlf=true it checks out CRLF — precisely the drift this stanza exists to prevent. Harmless so +# far only because git's clean filter normalizes before `git diff`, which is also why export sync must be +# verified with `git diff` and never a raw `diff` (the latter reports a phantom whole-file difference). +constraints.lock text eol=lf +# The PEP 735 CI-toolchain exports (ADR 0034 §3) — same DEP-1 diff gate, same LF requirement. +ci/locks/*.lock text eol=lf .mefor-hooks/** text eol=lf # Shell scripts are executed on the Linux CI runners; force LF so a contributor's # core.autocrlf=true can never commit CRLF (which breaks `#!/usr/bin/env bash\r`). diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2449496e..fd436e2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,9 +6,14 @@ updates: # pyproject.toml + uv.lock and REGENERATES uv.lock IN its PRs (version updates GA 2025-03, # security updates GA 2025-12) — the old "pip" ecosystem updated requirements/pyproject but NOT # uv.lock. It still does not re-derive the EXPORTED locks (requirements.lock + docker/locks/* + - # constraints.lock), which the DEP-1 gate in security.yml byte-diffs; + # constraints.lock + ci/locks/* — SIX artifacts), which the DEP-1 gate in security.yml byte-diffs; # .github/workflows/dependabot-lock-resync.yml re-exports those on the Dependabot branch so the # gate stays green. + # + # UNVERIFIED, and it cannot be verified before the next weekly run: whether this ecosystem enumerates + # PEP 735 `[dependency-groups]` (the hash-pinned CI toolchain, ADR 0034 §3) at all. If it does not, + # those pins go stale silently — which is why security.yml's pip-audit step audits ci/locks/*.lock + # directly, turning "silently stale" into a red gate within ~24h via the daily cron. - package-ecosystem: "uv" directory: "/" schedule: diff --git a/.github/workflows/dependabot-lock-resync.yml b/.github/workflows/dependabot-lock-resync.yml index eeb188c9..6ce3d315 100644 --- a/.github/workflows/dependabot-lock-resync.yml +++ b/.github/workflows/dependabot-lock-resync.yml @@ -1,17 +1,22 @@ name: Dependabot lock resync -# Re-exports the FOUR committed "uv export" artifacts (requirements.lock + +# Re-exports the SIX committed "uv export" artifacts (requirements.lock + # docker/locks/requirements-core.lock + docker/locks/requirements-sqlserver.lock + the HASHLESS -# constraints.lock) on a Dependabot PR that touched uv.lock / pyproject.toml, and commits them back -# to the PR branch so the DEP-1 drift gate (security.yml -> pip-audit job, step "Check the lockfile -# is in sync with pyproject (DEP-1)") goes green WITHOUT a human re-export. +# constraints.lock + the two PEP 735 CI-toolchain locks ci/locks/ci-scanners.lock and +# ci/locks/ci-quality.lock) on a Dependabot PR that touched uv.lock / pyproject.toml, and commits them +# back to the PR branch so the DEP-1 drift gate (security.yml -> pip-audit job, step "Check the +# lockfile is in sync with pyproject (DEP-1)") goes green WITHOUT a human re-export. # # WHY: the native "uv" Dependabot ecosystem regenerates uv.lock + pyproject.toml in its PR, but NOT -# the exported lock artifacts. The DEP-1 gate re-runs "uv lock --check" + the four "uv export"s and +# the exported lock artifacts. The DEP-1 gate re-runs "uv lock --check" + all six "uv export"s and # "git diff --exit-code"s the result, so a Dependabot uv PR would otherwise leave the exports stale # and red the gate. This workflow runs the IDENTICAL commands and pushes the refreshed exports onto # the Dependabot branch. # +# The two ci/locks/*.lock artifacts are what makes the CI toolchain hash-pinned rather than merely +# version-pinned (ADR 0034 §3). They are ALSO what keeps it fresh: a hash-pinned toolchain outside this +# resync machinery would rot into a pinned, stale, unpatched toolchain — worse posture than floating. +# # KEEP THIS LIST IN LOCKSTEP WITH security.yml's DEP-1 step. A file the gate diffs but this job does # not re-export is un-fixable by the bot: the resync pushes an incomplete set, DEP-1 re-exports the # full set, finds the missing one stale, and the PR stays red with no human-free path to green. @@ -128,6 +133,10 @@ jobs: # The hashless constraint export every CI install pins against (`--constraint constraints.lock`, # ci.yml). Same lock, --no-hashes; omitted here it would stay stale and red DEP-1 forever. uv export --all-extras --no-emit-project --no-hashes --format requirements.txt -o constraints.lock + # The CI TOOLCHAIN locks (PEP 735 dependency groups, ADR 0034 §3). Same lockstep rule as the + # four above: the gate diffs them, so the bot must re-export them or the PR has no path green. + uv export --only-group ci-scanners --format requirements.txt -o ci/locks/ci-scanners.lock + uv export --only-group ci-quality --format requirements.txt -o ci/locks/ci-quality.lock - name: Commit and push the resynced locks if: steps.creds.outputs.present == 'true' @@ -139,11 +148,11 @@ jobs: set -euo pipefail git config user.name 'dependabot[bot]' git config user.email '49699333+dependabot[bot]@users.noreply.github.com' - if git diff --quiet -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock; then + if git diff --quiet -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock; then echo 'Exported lock files already in sync; nothing to push.' exit 0 fi - git add requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock + git add requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock git commit -m 'chore(deps): resync exported lock files (DEP-1)' # Push with the persisted App-token credential (NOT GITHUB_TOKEN). An App-token push emits a # synchronize event so the required checks (DEP-1, ci.yml) re-run on the new commit; a diff --git a/.github/workflows/quality-advisory.yml b/.github/workflows/quality-advisory.yml index dedbf02c..cd96c88b 100644 --- a/.github/workflows/quality-advisory.yml +++ b/.github/workflows/quality-advisory.yml @@ -298,12 +298,26 @@ jobs: - name: Install project + coverage tools run: | uv pip install --system --constraint constraints.lock -e ".[dev,harness,fhir,dicom,x12,xml,webauthn]" -e packaging/messagefoundry-webconsole - # NEITHER package is in constraints.lock, so the --constraint above is a no-op for them and both - # previously floated to whatever PyPI served that day. diff-cover is now pinned exactly: the - # inline-annotation surface depends on `--format github-annotations:` and on adjacent-line - # coalescing in GitHubAnnotationsReportGenerator. Deliberately NOT in pyproject.toml -- these are - # CI-only tools, and adding them would trip the DEP-1 lock-sync gate and force four re-exports. - uv pip install --system --constraint constraints.lock pytest-cov "diff-cover==10.4.1" + # HASH-PINNED from the CI toolchain lock (ADR 0034 §3). These tools now DO live in + # pyproject.toml ([dependency-groups].ci-quality, where each pin's rationale sits) and DEP-1 + # re-exports this lock by design -- superseding the note that used to sit here claiming they + # were deliberately kept out of pyproject to avoid tripping that gate. + # + # `python -m pip`, not `uv pip install --system`, for ONE honest reason: every other hashed + # install in this repo is a pip install, and keeping one spelling means one thing for the + # guards in tests/test_ci_venv_pinning.py to match. Same setup-python interpreter either way. + # + # NOT because uv cannot: `uv pip install --require-hashes` exists (env UV_REQUIRE_HASHES), and + # setup-uv is already in this job -- the earlier claim that "--require-hashes is pip's + # contract" was simply wrong. NOR for Scorecard's benefit: its parser keys on + # pip/pip3/`python -m pip`, so a `uv pip` line generates NO finding at all, and converting one + # can only ADD parseable surface -- it cannot close an alert. This line is posture-neutral to + # Scorecard and is here for uniformity. + # + # Safe against the editable install above: the group lock and constraints.lock agree on all 10 + # shared packages (pytest and pytest-timeout among them), so this cannot re-point what the + # coverage run executes under. + python -m pip install --require-hashes -r ci/locks/ci-quality.lock - name: Tests under coverage (advisory - never fails) continue-on-error: true env: @@ -428,13 +442,17 @@ jobs: - name: Install project + mutmut run: | uv pip install --system --constraint constraints.lock -e ".[dev,harness,fhir,dicom,x12,xml,webauthn]" -e packaging/messagefoundry-webconsole - # mutmut 3, pinned. `mutmut<3` resolved to 2.5.1, which CRASHES on Python 3.14 in its pony-ORM - # cache (`cannot pickle 'itertools.count'`) before generating a single mutant -- verified from - # run 30248096425. pytest-timeout is REQUIRED: mutmut 3 always passes `--timeout`/ - # `--timeout-method` to pytest, and without the plugin every test invocation dies with an - # unrecognised-argument error that surfaces only as BadTestExecutionCommandsException. - # Neither package is in constraints.lock, so the --constraint above does not pin them. - uv pip install --system "mutmut==3.6.0" pytest-timeout + # mutmut 3 + pytest-timeout, HASH-PINNED from the CI toolchain lock (ADR 0034 §3). Both live in + # pyproject.toml's [dependency-groups].ci-quality, which records why each is load-bearing: + # `mutmut<3` resolved to 2.5.1, which CRASHES on Python 3.14 in its pony-ORM cache + # (`cannot pickle 'itertools.count'`) before generating a single mutant -- verified from run + # 30248096425; and pytest-timeout is REQUIRED because mutmut 3 always passes `--timeout`/ + # `--timeout-method` to pytest, so without the plugin every invocation dies with an + # unrecognised-argument error surfacing only as BadTestExecutionCommandsException. + # `python -m pip` for UNIFORMITY with every other hashed install in this repo, not because uv + # cannot do it (`uv pip install --require-hashes` exists) and not for Scorecard (a `uv pip` + # line generates no finding at all) -- see the coverage job's install step for the full note. + python -m pip install --require-hashes -r ci/locks/ci-quality.lock - name: Mutation-test a bounded scope (advisory - never fails) continue-on-error: true env: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index b80be034..8a60ae51 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -78,7 +78,12 @@ jobs: # same lock, so a fresh `uv pip install -e ".[extras]"` cannot drift to a newer upstream # release. Re-export it here so it can never fall out of step with uv.lock. uv export --all-extras --no-emit-project --no-hashes --format requirements.txt -o constraints.lock - git diff --exit-code -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock + # The CI TOOLCHAIN locks (ADR 0034 §3): PEP 735 `[dependency-groups]`, so the scanners and the + # quality tools flow through uv.lock and are consumed HASH-PINNED. Non-default groups, so they + # stay out of the four exports above (and out of the SBOM / image locks / audited runtime). + uv export --only-group ci-scanners --format requirements.txt -o ci/locks/ci-scanners.lock + uv export --only-group ci-quality --format requirements.txt -o ci/locks/ci-quality.lock + git diff --exit-code -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock - name: Install from the hashed lockfile (DEP-1) run: | # --require-hashes enforces a hash for every requirement (the lockfile carries them): a @@ -89,13 +94,33 @@ jobs: /tmp/lockcheck/bin/pip install --require-hashes -r requirements.lock - name: Audit the locked dependencies (DEP-1) run: | - # PINNED. This job exists to prove nothing unpinned enters the tree, and it was installing - # its own auditor with a bare `pip install pip-audit` — dependency intake that none of the - # lockfiles it checks actually covers. `==` also makes the audit reproducible: an unpinned - # auditor can change its advisory database handling between two runs of the same commit. - python -m pip install --upgrade pip "pip-audit==2.10.1" + # HASH-PINNED from the lock, not merely `==`-pinned: a version pin does not satisfy Scorecard + # PinnedDependenciesID, and the transitive closure floated regardless. Rationale for each pin + # lives in pyproject.toml's [dependency-groups]. THIS step's `--upgrade pip` bootstrap is gone + # rather than pinned — the lock hash-pins `pip` itself (a pip-audit → pip-api dependency). + # Scoped deliberately, because the unqualified claim was FALSE for this job: the DEP-1 step + # above still runs `pip install --upgrade pip "uv=="` in this same interpreter two steps + # earlier, and that unverified pip does the load-bearing work (the six exports + the diff + # gate); the line below then DOWNGRADES pip to the locked version after the fact. So this + # job's posture improves for the AUDITOR, not for the exporter. The `uv` bootstrap is circular + # by construction (ADR 0034 section 3's residuals) and is why `pip` stays registered in + # SECURITY_YML_ACCEPTED_UNPINNED. The claim holds unqualified in the bandit job and zizmor.yml. + python -m pip install --require-hashes -r ci/locks/ci-scanners.lock # Audit the committed, pinned set — reproducible (vs auditing a fresh latest-resolve). pip-audit -r requirements.lock --desc + # Audit the TOOLCHAIN locks too. NOT bookkeeping: hash-pinning makes the toolchain STICKY, so + # without this a CVE in a pinned scanner is invisible to every gate — the "pinned, stale, + # unpatched is worse than floating" failure ADR 0034 section 3 names. `--ignore-vuln ` is + # the escape hatch for a triaged advisory, exactly as for the lockfile audit above. + # + # CONSEQUENCE, stated rather than discovered later: this is a REQUIRED context, so a CVE in + # mutmut's or diff-cover's closure reds the merge gate over an ADVISORY tool. That is not a new + # posture — requirements.lock is exported `--all-extras`, so ruff, mypy and pytest already do + # exactly this — but it does add 40 distributions to the blocking set (23 from ci-scanners, 17 + # from ci-quality; measured). Zero advisories at the time of writing. If the owner would rather + # the advisory half not block, move the ci-quality line alone to its own step. + pip-audit -r ci/locks/ci-scanners.lock --desc + pip-audit -r ci/locks/ci-quality.lock --desc # ANTI-SLOPSQUAT. Deliberately a STEP in this already-REQUIRED job rather than a new context: it # blocks today with no branch-protection change, the same reasoning as the ledger-gate backstop # in ci.yml. (A hard-failing job that is NOT a required context does not stop auto-merge — it @@ -300,9 +325,10 @@ jobs: python-version: "3.14" - name: Scan source for insecure patterns run: | - # Pin bandit: an unpinned upgrade once silently changed `# nosec` parsing (1.9.x requires - # space-separated test IDs) and broke a green branch. Bump deliberately, not implicitly. - python -m pip install --upgrade pip "bandit==1.9.4" + # HASH-PINNED from the lock (ADR 0034 §3); bandit's version is the findings baseline of a + # blocking gate — the `# nosec`-parsing incident behind that pin is recorded beside the pin + # itself, in pyproject.toml's [dependency-groups].ci-scanners. Bump there, then re-export. + python -m pip install --require-hashes -r ci/locks/ci-scanners.lock # Skips are reviewed-safe project idioms. The subprocess sites (B603/B607) are annotated # per-line with `# nosec` at the call instead of a wholesale skip, so a NEW unreviewed # subprocess call is still flagged (review low-27): diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 807a6e0c..99a00a02 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -36,10 +36,11 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - - name: Install zizmor (pinned) + - name: Install zizmor (hash-pinned from the CI toolchain lock) run: | - # Pin deliberately; verify the tag at https://github.com/zizmorcore/zizmor/releases if it 404s. - python -m pip install --upgrade pip "zizmor==1.5.2" + # HASH-PINNED from the lock (ADR 0034 §3). The version lives in pyproject.toml's + # [dependency-groups].ci-scanners with its rationale; bump it there, then re-export. + python -m pip install --require-hashes -r ci/locks/ci-scanners.lock # actionlint FIRST: it catches the class zizmor structurally cannot. An invalid `${{ }}` anywhere # in a `run:` body -- comments included -- aborts workflow COMPILATION, so no jobs are created and # every required context silently never appears (the PR looks stuck, not red). zizmor parses the diff --git a/ci/locks/ci-quality.lock b/ci/locks/ci-quality.lock new file mode 100644 index 00000000..45e1ed83 --- /dev/null +++ b/ci/locks/ci-quality.lock @@ -0,0 +1,233 @@ +# This file was autogenerated by uv via the following command: +# uv export --only-group ci-quality --format requirements.txt -o ci/locks/ci-quality.lock +chardet==7.4.3 \ + --hash=sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea \ + --hash=sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e \ + --hash=sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93 \ + --hash=sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04 \ + --hash=sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7 \ + --hash=sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c \ + --hash=sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4 \ + --hash=sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c \ + --hash=sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e \ + --hash=sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56 \ + --hash=sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11 \ + --hash=sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531 + # via diff-cover +click==8.4.1 \ + --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \ + --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96 + # via mutmut +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # click + # pytest +coverage==7.15.2 \ + --hash=sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2 \ + --hash=sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c \ + --hash=sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a \ + --hash=sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145 \ + --hash=sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138 \ + --hash=sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c \ + --hash=sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071 \ + --hash=sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a \ + --hash=sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d \ + --hash=sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a \ + --hash=sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0 \ + --hash=sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658 \ + --hash=sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072 \ + --hash=sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199 \ + --hash=sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446 \ + --hash=sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743 \ + --hash=sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be \ + --hash=sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7 \ + --hash=sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984 \ + --hash=sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc \ + --hash=sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee \ + --hash=sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3 \ + --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f \ + --hash=sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5 \ + --hash=sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1 \ + --hash=sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688 \ + --hash=sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d \ + --hash=sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328 \ + --hash=sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635 \ + --hash=sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c \ + --hash=sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243 \ + --hash=sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a + # via + # mutmut + # pytest-cov +diff-cover==10.4.1 \ + --hash=sha256:0ec566955c9ee7da2f6cc48fa16fac7f97ad1fc4e50a887ffb9cfe5eb1e831df \ + --hash=sha256:dc8f2654c485ec4f16e679b5af6e205783cde71185d4ceb8157662dca2d531e9 +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via diff-cover +libcst==1.9.0 \ + --hash=sha256:087b58a9afe076bb08e2d726478e1f16cb928d67ffa9092817e033c335de522a \ + --hash=sha256:0c7b548512db25af9c2997a95fa731bd6b6928ecbad6c0915d7482d8bb42d34f \ + --hash=sha256:107593af46945593e7825821793393262bc4fa1d3ea24c3ed487b61269bbbdf8 \ + --hash=sha256:32395244edfe6538e0ea2bf82051d60103d3f54861274805c4fb3745efb70a85 \ + --hash=sha256:444e84c76cd035cd2fe136838c1a524d34b08216521f6f5093df8a6f6cfa5799 \ + --hash=sha256:45808c03528b3ad40b14095348a918e08d98c47b4a125d631cb78e817c0a5b16 \ + --hash=sha256:496c24e0d3240bc7da45dae543aff3f5b6509978c39262d6b84ed2fb999dded2 \ + --hash=sha256:497d5329345f1f5df84e41b0bbd00204b64a2fd30dfe3cfaeaebca633a31e877 \ + --hash=sha256:50ab94bb2524b419056d4003032b8c67102ac800f8d85b3c4260a01746d94dbb \ + --hash=sha256:568288cdbfe3b4ca3ae4852cb0a439ff053dd54c841bc4995bf2b71238b5de40 \ + --hash=sha256:a2faaf92500d0226358125630f5aab4758e8aad3f2d70a10892ec3c700781a54 \ + --hash=sha256:bb5d0946f2b4c6711b5d69fe4f833b364f9e7a1a2b08f88b98619dc18975099a \ + --hash=sha256:ea490fa8540503db5f321f0268becab46eb50f710e8cec8041e241fd66f6874f \ + --hash=sha256:eccf4c57d273cdd3fe1c67b72cf9bb1bbd4547aa011824e96ccf5b7136057aa4 \ + --hash=sha256:f6248cb07444ab9a6733a855737a9febed8b9adca51347019348b09a3ac7dfe9 + # via mutmut +linkify-it-py==2.1.0 \ + --hash=sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e \ + --hash=sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b + # via markdown-it-py +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via + # mdit-py-plugins + # rich + # textual +markupsafe==3.0.3 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +mdit-py-plugins==0.6.1 \ + --hash=sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d \ + --hash=sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0 + # via textual +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +mutmut==3.6.0 \ + --hash=sha256:a9f5b8dcf6cbf9496769d7cf8bdbba37a0ec709ad98f88d103238b62f10bdf37 \ + --hash=sha256:bcbd3e4d0d2d4edf3dfb42955417279a8866a3dbbcb87d619f2f3fd0ac7fafda +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via pytest +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 + # via textual +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via + # diff-cover + # pytest + # pytest-cov +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via + # diff-cover + # pytest + # rich + # textual +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via + # mutmut + # pytest-cov + # pytest-timeout +pytest-cov==7.1.0 \ + --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ + --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 +pytest-timeout==2.4.0 \ + --hash=sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a \ + --hash=sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2 +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b + # via libcst +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via textual +setproctitle==1.3.7 \ + --hash=sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2 \ + --hash=sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b \ + --hash=sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e \ + --hash=sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73 \ + --hash=sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070 \ + --hash=sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5 \ + --hash=sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed \ + --hash=sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416 \ + --hash=sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4 \ + --hash=sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309 \ + --hash=sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3 \ + --hash=sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17 \ + --hash=sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63 \ + --hash=sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1 \ + --hash=sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0 \ + --hash=sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e \ + --hash=sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8 \ + --hash=sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e \ + --hash=sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a \ + --hash=sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45 \ + --hash=sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f + # via mutmut +textual==8.2.8 \ + --hash=sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a \ + --hash=sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b + # via mutmut +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via textual +uc-micro-py==2.0.0 \ + --hash=sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c \ + --hash=sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811 + # via linkify-it-py diff --git a/ci/locks/ci-scanners.lock b/ci/locks/ci-scanners.lock new file mode 100644 index 00000000..471ed1fb --- /dev/null +++ b/ci/locks/ci-scanners.lock @@ -0,0 +1,234 @@ +# This file was autogenerated by uv via the following command: +# uv export --only-group ci-scanners --format requirements.txt -o ci/locks/ci-scanners.lock +bandit==1.9.4 \ + --hash=sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628 \ + --hash=sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e +boolean-py==5.0 \ + --hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \ + --hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9 + # via license-expression +cachecontrol==0.14.4 \ + --hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \ + --hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1 + # via pip-audit +certifi==2026.6.17 \ + --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ + --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db + # via requests +charset-normalizer==3.4.7 \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 + # via requests +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via bandit +cyclonedx-python-lib==11.11.0 \ + --hash=sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44 \ + --hash=sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102 + # via pip-audit +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via py-serializable +filelock==3.32.2 \ + --hash=sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82 \ + --hash=sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8 + # via cachecontrol +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via requests +license-expression==30.4.4 \ + --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \ + --hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd + # via cyclonedx-python-lib +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via rich +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +msgpack==1.2.1 \ + --hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \ + --hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \ + --hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \ + --hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \ + --hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \ + --hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \ + --hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \ + --hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \ + --hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \ + --hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \ + --hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \ + --hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \ + --hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \ + --hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \ + --hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \ + --hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \ + --hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \ + --hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \ + --hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \ + --hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \ + --hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \ + --hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \ + --hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c + # via cachecontrol +packageurl-python==0.17.6 \ + --hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \ + --hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9 + # via cyclonedx-python-lib +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via + # pip-audit + # pip-requirements-parser +pip==26.2 \ + --hash=sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690 \ + --hash=sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad + # via pip-api +pip-api==0.0.34 \ + --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ + --hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625 + # via pip-audit +pip-audit==2.10.1 \ + --hash=sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc \ + --hash=sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a +pip-requirements-parser==32.0.1 \ + --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \ + --hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3 + # via pip-audit +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 + # via pip-audit +py-serializable==2.1.0 \ + --hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \ + --hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304 + # via cyclonedx-python-lib +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via rich +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc + # via pip-requirements-parser +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b + # via bandit +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # cachecontrol + # pip-audit +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via + # bandit + # pip-audit +sortedcontainers==2.4.0 \ + --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ + --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 + # via cyclonedx-python-lib +stevedore==5.9.0 \ + --hash=sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c \ + --hash=sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7 + # via bandit +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf + # via pip-audit +tomli-w==1.2.0 \ + --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ + --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 + # via pip-audit +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests +zizmor==1.5.2 \ + --hash=sha256:03b27c46d87e96a0acdf78190fbb674c174f67ded396c74dadd64d7c4ecba680 \ + --hash=sha256:0c5f656106c430748858ae459c41c9eed09a1e01e5f42015d80c8fe34740d173 \ + --hash=sha256:506b355c693d40df01bdf26fd1dfcb41801af7b8c51767d4159efdc2655965c4 \ + --hash=sha256:5e18b06af4208624ef707e7859b07539870ca26b74a89537e1a6bfba2e44fc9a \ + --hash=sha256:848f04c0d84b085dfb79c66951404372ebf9b3dd0c73076f1baac83db29e74e4 \ + --hash=sha256:86395dd985ed6bf9acffea8a900b30d8d4dd4c9e20421f16f2db7ad92299e24e \ + --hash=sha256:8c65b4dbc9690d3f0f5d9756ae0b5ca8d3dfb4f806d82a5195836a3b613ae996 \ + --hash=sha256:9d40780b19da7901423de80ccce083a8c0d2114e0cf6432aa20d8e60d15e97f6 \ + --hash=sha256:a40feabc2c043aca60f9edbd35a676d97d684af5b06ed61a7752869c79b3be30 \ + --hash=sha256:dcef697a88983e7ce6948df1603e2325e2cf5c523828af94615e6c432fa8d98a \ + --hash=sha256:df1326fe9f9ddcbe9d862a97571c848276882d0ceeee39e18fcfc9ec5c66cd7b diff --git a/docs/Code_Quality_Standards.md b/docs/Code_Quality_Standards.md index adfa56bd..9e4d936c 100644 --- a/docs/Code_Quality_Standards.md +++ b/docs/Code_Quality_Standards.md @@ -311,6 +311,8 @@ Ordered by anti-slop leverage, not effort (build placement per §5). **✅ = shi **Rollout record (measured 2026-07-13 — how the \#1047 sweep was executed):** `B,C4,SIM,UP,I` = **853 violations** (238 `B008` FastAPI false positives to exclude; 111 `I001` repo-wide import reorder); `C901` = **85 hits**. Safe rollout: (a) exclude framework-idiom rules (`B008` on `api/`); (b) **grandfather** the existing backlog so the *required* gate stays green (per-file-ignores / ratchet — new code only); (c) run the repo-wide import sort as a **dedicated pass when parallel worktrees are quiescent** — a 100+-file sweep conflicts with in-flight sessions; (d) keep `C901` **advisory**. (The built coverage/mutation gates install their tools CI-side via an ephemeral `uv pip install`, so they needed **no** `requirements.lock` change — DEP-1 unaffected.) +> **Correction, 2026-07-29 (ADR 0034 §3).** Both halves of that last parenthesis are now false, and this is the copy that escaped the sweep — `quality-advisory.yml`'s own version of the claim was rewritten in the same change. `pytest-cov`, `diff-cover`, `mutmut` and `pytest-timeout` moved into `pyproject.toml`'s PEP 735 `[dependency-groups].ci-quality` and are installed from a hash-pinned `uv export` (`python -m pip install --require-hashes -r ci/locks/ci-quality.lock`), because a *version* pin does not satisfy Scorecard's `PinnedDependenciesID`. So `pyproject.toml` **did** change, and the lock lives **inside** the DEP-1 export/diff machinery rather than beside it (a hash-pinned toolchain outside it rots into a pinned, stale, *unpatched* one — worse posture than floating). `requirements.lock` itself is genuinely unaffected: the groups are deliberately **non-default**, so they stay out of all four pre-existing exports — verified by `git diff --exit-code`, and now guarded by `test_dependency_groups_do_not_leak_into_the_runtime_exports`. + ### A.4 Documented caveat — solo-maintainer review Row 5's "human review" is **self-review** (the [SDS §A.6](Secure_Development_Standards.md#a6-documented-deviations) / [AI companion Appendix A.6](Secure_AI_Development_Standards.md#a6-documented-deviations) single-maintainer deviation). The Stanford overconfidence finding (§3) bites hardest exactly when the author reviews their own AI-authored code — which is the strongest argument for the mutation gate (Built this cycle — \#1040), since it is the one control that *adversarially* checks whether the tests assert anything, independent of the author's confidence. diff --git a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md index c376bcbd..c96d4533 100644 --- a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md +++ b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md @@ -70,6 +70,8 @@ Scorecard runs on the same mirror and surfaced **48 findings**. These are **repo → `tests/test_multipart.py::test_oversized_part_header_is_refused_not_parsed` - **AC-7** — WHEN the multipart parser scans a `Content-Disposition` line, THE SYSTEM SHALL do so in time linear in the line's length. → `tests/test_multipart.py::test_hostile_disposition_header_parses_in_linear_time` +- **AC-8** — WHERE a CI job installs a third-party quality or security tool that this repo routes through a PEP 735 dependency group, THE SYSTEM SHALL install it from a hash-pinned `uv export` with `--require-hashes`, and THAT lock SHALL be regenerated and diff-gated by the DEP-1 step rather than hand-maintained. + → `tests/test_ci_venv_pinning.py` (`test_lock_installed_toolchain_*`, `test_moved_tools_are_declared_in_a_dependency_group`, `test_no_moved_tool_is_reinstalled_inline`) · `tests/test_dep1_lock_resync_lockstep.py` ## Options considered @@ -253,3 +255,181 @@ same way; suppressing this rule for a scanner is the industry-standard dispositi **Convergence note (per the rule above):** the anchor is `tls_probe.py:146`, inside `_offer_context`. That module is new and small, so expect this to re-fire as a fresh alert number the first time anything is inserted above line 146. Re-dismiss with this rationale rather than re-triaging from scratch. + +## Amendment — 2026-07-29: §3's convergence target is BUILT — the CI toolchain is hash-pinned + +§3 above ("A version pin does not satisfy this check") named the fix and deferred it: *"The correct fix +is to route CI tooling through a `pyproject` dependency group so it flows into `uv.lock` and the +exports."* That is now built. **This section supersedes §3's "Deferred, recorded here as the convergence +target" and the "Option B … remains the only thing that closes the alert" sentence in *Recommended +hardening*, and it retires the "four committed lock artifacts" count — there are now six.** + +### What is now genuinely hash-pinned + +`pyproject.toml` gained a PEP 735 `[dependency-groups]` table with two groups, exported to two new +committed artifacts and consumed with `--require-hashes`: + +| Group | Lock | Installed by | Contents | +|---|---|---|---| +| `ci-scanners` | `ci/locks/ci-scanners.lock` (33 reqs, 163 hashes) | `security.yml` pip-audit + bandit jobs, `zizmor.yml` | `bandit==1.9.4`, `pip-audit==2.10.1`, `zizmor==1.5.2` | +| `ci-quality` | `ci/locks/ci-quality.lock` (27 reqs, 164 hashes) | `quality-advisory.yml` coverage + mutation jobs | `diff-cover==10.4.1`, `mutmut==3.6.0`, `pytest-cov>=7.0`, `pytest-timeout>=2.3` | + +Four design decisions worth recording, because each is a place a later change could silently undo the +posture: + +1. **The split is the merge path.** `ci-scanners` is what the *blocking* gates install for themselves; + `ci-quality` is *advisory* measurement. This keeps `mutmut` — a mutation engine that rewrites and + executes source — out of every required gate's dependency closure. +2. **Not extras, and not `[tool.uv] default-groups`.** An extra is published wheel metadata, so + `pip install messagefoundry[ci-scanners]` would become a real install target; a dependency group + never ships. A *default* group would land in all four pre-existing DEP-1 artifacts — i.e. in the + release SBOM, the container image locks, and in what `pip-audit` audits **as runtime**. Verified: + with these groups non-default, all four re-export byte-identically (`git diff --exit-code` → 0). +3. **The locks are inside the DEP-1 machinery, not beside it.** §3's own objection to a fifth lock was + that a hand-maintained one "would rot into a pinned, **stale, unpatched** toolchain — worse posture + than floating". So both are `uv export`ed by `security.yml`'s DEP-1 step, diff-gated there, and + re-exported + staged by `dependabot-lock-resync.yml`. `tests/test_dep1_lock_resync_lockstep.py` + enforces the six-place lockstep; `test_lock_installed_toolchain_locks_are_in_the_dep1_set` enforces + that a lock the workflows install is one the gate regenerates. +4. **`pip-audit` now audits the toolchain locks too** (`pip-audit -r ci/locks/*.lock --desc`). This is + the load-bearing half of decision 3: hash-pinning makes a toolchain *sticky*, so without this a CVE + in a pinned scanner would be invisible to every gate. `--ignore-vuln ` is the escape hatch, as + for the runtime lock. + + **Consequence, and the measurement that frames it — this one is the owner's to confirm.** `pip-audit` + is a *required* context, so a CVE in `mutmut`'s or `diff-cover`'s closure will red the merge gate over + an **advisory** tool. Two facts bound how novel that is. It is **not a new posture**: + `requirements.lock` is exported `--all-extras`, so `ruff`, `mypy`, `pytest` and `pytest-timeout` + already sit in the blocking audit's input — the repo has always blocked merges on CVEs in dev + tooling. But it **is** a wider blast radius: **40** distributions are new to the blocking set (23 via + `ci-scanners` — `bandit`, `zizmor`, `pip-audit`, `cachecontrol`, `rich`, `msgpack`, … — and 17 via + `ci-quality` — `mutmut`, `diff-cover`, `pytest-cov`, `jinja2`, `markupsafe`, `coverage`, `libcst`, + `textual`, …). Measured at the time of writing: **0 advisories** across all 60 `name==version` pairs + (`pip-audit` on both locks, exit 0; independently cross-checked against OSV `querybatch`). If the + advisory half should not block, the one-line change is to move `pip-audit -r ci/locks/ci-quality.lock` + into its own step — keeping the *scanners*' audit blocking, which is the same "the split is the merge + path" line decision 1 already draws. + +Two unpinned `pip install --upgrade pip` bootstraps disappeared rather than being pinned: the +`ci-scanners` lock hash-pins `pip==26.2` itself (it arrives as a `pip-audit` → `pip-api` dependency), so +a hash-verified pip now lands in the same command that previously fetched an unverified one. **Two +survive** in `security.yml` — the `uv` bootstrap and the `semgrep` step, both named in the residuals +below and counted by `test_security_yml_pip_bootstrap_count_is_exact` so the number cannot drift out of +this prose again. + +### Which findings this closes + +Three Scorecard-visible `pip install` lines became `--require-hashes` installs: + +| Line | Alert | Note | +|---|---|---| +| `security.yml` bandit step | **#74** | the exact proof-case §3 cites | +| `zizmor.yml` zizmor install | **96** | §3's second proof-case | +| `security.yml` pip-audit step | whichever anchors that line | previously `pip-audit==2.10.1`, unhashed | + +**Stated honestly: this does not reduce the OPEN count.** #74 and 96 are in the **dismissed +(`won't fix`)** set, so the effect is that three dismissals whose recorded reason ("CI installs +editably, which cannot use `--require-hashes`") is now *false for those lines* become re-triageable **as +fixed**. Under this ADR's own Decision — a dismissal with a false reason is worse than an open finding — +that is the point, not a consolation. + +The **2 genuinely open** `PinnedDependenciesID` alerts are untouched: they are the medium pair on the +**SBOM scratch venv** (`python -m pip install "pip==26.1.2" "cyclonedx-bom~=7.3.1"`, in `release.yml` +and `security.yml`). Closing them needs a third group *and* moving **both** halves in lockstep, because +`test_sbom_install_is_byte_identical_in_release_and_security` requires the two commands to stay +identical. `release.yml` was deliberately scoped out of this change (see the residuals), so this is +recorded as the next increment rather than done. + +### Residuals — dismissals that stay dismissed, and why + +A `won't fix` makes an item invisible, so each remaining one is named with its reason rather than +implied: + +| Residual | Why it is not fixed here | +|---|---| +| **`release.yml`'s `sigstore==4.4.0`** | **Not a gap — an owner decision this change must not invert.** Routing `sigstore` through the lock **would** resolve it to **4.5.0** — stated counterfactually because that is what it is: `sigstore` is deliberately **absent** from `uv.lock` and from all six exports (0 hits), and the 4.5.0 figure was measured in a scratch resolve. A reader who greps `uv.lock`, finds nothing, and concludes this row is stale would re-open the very decision it exists to preserve. ADR 0034 pins 4.4.0 because `.github/dependabot.yml` sets `cooldown.default-days: 5` and 4.5.0 was <48 h old. Routing `sigstore` through the lock would pin the *signing* toolchain fresher than the repo's own update policy allows — inverting that policy at the highest-privilege point in the build (the job holding `contents`/`id-token`/`attestations: write`). Left exactly as it is. Re-evaluate when 4.5.0 ages out. | +| **The `uv` bootstrap** (`security.yml`, `python -m pip install --upgrade pip "uv==0.12.0"`) | **Permanently circular: you cannot hash-lock `uv` with `uv`.** That install produces every lock this repo commits. `uv` stays an inline `==` pin, and `pip` remains the sole registered *name* in `SECURITY_YML_ACCEPTED_UNPINNED`. Note it is also the pip that runs the **six exports and the diff gate** — the `--require-hashes` install two steps later *downgrades* pip to the locked version afterwards, so the DEP-1 step's own posture is unchanged by this work. *Cheap out-of-band fix that removes it entirely:* `astral-sh/setup-uv@c771a70e…` is already SHA-pinned and used in 9 places (`ci.yml` ×6, `quality-advisory.yml` ×2, the resync ×1); swapping it in deletes the install. Separate change. | +| **`security.yml`'s unpinned `pip` in the `semgrep` step** — `python -m pip install --upgrade pip "semgrep==1.172.0"` | **The SECOND surviving bootstrap, named because an undercounted inventory is how a real finding goes invisible.** The semgrep row below explains only the `[otel]` conflict that keeps *semgrep* inline; this row records that the same line is also an **unpinned `pip` fetch**. So two `--upgrade pip` bootstraps remain in the file, not one — now asserted as an exact count by `test_security_yml_pip_bootstrap_count_is_exact`, since `SECURITY_YML_ACCEPTED_UNPINNED` registers the *name* `pip` and cannot tell two accepted bootstraps from twenty. Mitigating: `semgrep` is **not** a required context (`tests/test_required_contexts.py`), so this one does not sit on the merge path. It disappears whenever the semgrep row's `[tool.uv] conflicts` recipe is taken. | +| **`quality-advisory.yml`'s `pipx install ruff`** | **Outside the guard's regex and outside Scorecard's.** `test_ci_venv_pinning.py`'s `_PIP_INSTALL` matches `pip`/`pip3`/`python -m pip` only, so the unpinned fallback branch is invisible to every existing guard — and because it is not a `pip install`, **no alert exists to close**. `pipx` has no `--require-hashes`, so fixing it means changing the install mechanism, not the pin. Recorded, not done. | +| **`semgrep`** | **Excluded by decision.** `semgrep==1.172.0` requires `opentelemetry-sdk>=1.37,<1.38` while the project's `[otel]` extra resolves 1.44. In a plain group the universal resolve silently **downgrades the shipped otel runtime** in all four pre-existing DEP-1 artifacts — measured and bisected to semgrep alone (the other tools give DIFFS=0). The only fix is `[tool.uv] conflicts = [[{ extra = "otel" }, { group = "semgrep-tools" }]]`, which declares a **product extra** and a **CI scanner** permanently mutually exclusive (`uv sync --all-extras --all-groups` would stop working) and still forces a `click 8.4.1 → 8.4.2` re-resolve across all four artifacts. Pinning a *scanner*'s supply chain is not worth a lasting constraint on a shipped surface. The recipe is written down here so a future owner can flip it in one commit rather than re-deriving the analysis. | +| **The 5 editable `pip install -e ".[…]"` sites** + 7 `uv pip install --system -e` sites | Structurally unhashable; §3's original rationale is correct for these and stands. | +| Docker digest-pinning (7), `TokenPermissions` (6), `BranchProtection`/`CodeReview`/`Maintained` (3), `Fuzzing`, `CIIBestPractices` | Different check classes; unchanged by this work. | + +### Two notes on evidence + +**Pre-merge coverage was the reason for the scope choice.** All three targeted workflows run on +`pull_request` — `security.yml`'s pip-audit and bandit jobs are *required contexts*, `zizmor.yml`'s +`paths: .github/**` filter matches this change, and `quality-advisory.yml`'s install steps are not +`continue-on-error`, so a bad hashed install reds the job visibly. That is the opposite of the +"What no test can see" problem the 2026-07-28 round had to accept, and it is why `release.yml` was left +alone: nothing in PR CI executes it, so a break there first surfaces at a tag. + +**The one gap, and its bound.** `dependabot-lock-resync.yml` triggers on `pull_request` for +`pyproject.toml`/`uv.lock`, but its `if:` requires `pull_request.user.login == 'dependabot[bot]'`, so it +**skips** on a human PR and there is no `workflow_dispatch` route. Its two new export lines are +text-verified only and first execute on the next Dependabot uv PR. The bound is the same one +`test_sbom_install_is_byte_identical_in_release_and_security` already relies on: +`test_export_flags_are_identical_per_lock_file` forces the resync's flag strings byte-identical to the +gate's, **and the gate does execute on this PR** — a dry-run is evidence for as long as the two are the +same command. + +**Unverified, and it cannot be verified before merge:** whether Dependabot's `uv` ecosystem enumerates +`[dependency-groups]` at all. First observation is the next weekly uv PR. If it does not, routine +staleness returns — which is precisely why the `pip-audit` addition (decision 4) is load-bearing: it +converts "silently stale" into "loudly red within ~24 h on anything security-relevant" via the daily +cron. Confirm after the next Dependabot run. + +### Convergence-rule consequence, stated rather than glossed + +**Measured, not estimated** (an earlier draft of this section guessed "~9", which would have understated +the re-anchor budget by two thirds — a wrong number here costs a re-triage, so count it): + +| Workflow | `origin/main` | this change | drift | first changed line | +|---|---|---|---|---| +| `security.yml` | 464 | 490 | **+26** | ~81 (the DEP-1 export step) | +| `quality-advisory.yml` | 619 | 637 | **+18** | ~301 (the coverage install step) | +| `dependabot-lock-resync.yml` | 141 | 150 | **+9** | ~1 (the header) | +| `zizmor.yml` | 89 | 90 | **+1** | ~39 (the install step) | + +Per the line-drift rule above, **every dismissed alert anchored below the first changed line re-fires as +a new alert number** — up to 26 lines of drift in `security.yml`. That is unavoidable when adding exports +to an early step. The mitigation applied: each *pin's* rationale moved to `pyproject.toml` beside the pin +itself (which is also where a future bumper will look), so the workflow comments carry only what is +specific to the call site. **No claim of line-neutrality is made here** — expect to re-dismiss, and +re-dismiss with the rationale in this section rather than re-triaging from scratch. Re-measure this table +if the change is rebased; do not carry the numbers forward on faith. + +### What the adversarial pass found in the guards, and what now enforces it + +Three reviews attacked this change before it landed. None found a wrong byte in a lock — `uv lock --check` +exit 0, all six exports byte-identical, the runtime closure a `588 insertions / 0 deletions` diff (a +version or edge change is impossible without a deletion). Every finding was a **guard gap**: a rule the +prose asserted and nothing checked. Each was reproduced by injecting the regression and confirming the +suite stayed **green**, then closed and confirmed **red**: + +| Hole | What passed green with the regression in place | Now enforced by | +|---|---|---| +| Moving `bandit`/`pip-audit` out of `RELEASE_PINNED_TOOLS` deleted the only check that rejected a **floor** | all three scanner specs rewritten to `>=` → re-locked, **re-exported byte-identically**, whole suite green. `uv export` writes `bandit==1.9.4` from `bandit>=1.9.4`, so lock-side checks are structurally blind | `EXACT_GROUP_PINS` / `FLOOR_BY_DESIGN` + `test_moved_tool_pins_are_exact_not_floors` — asserted at the **declaration**, where the decision lives | +| The export **selector** was unpinned (`uv export .* -o `) | `--group` for `--only-group` grew `ci-scanners.lock` from **33 → 69** requirements, pulling `fastapi`/`uvicorn`/`hl7`/`httpx`/`aiosqlite` into the **blocking** bandit and zizmor jobs — fully pinned, fully hashed, byte-identical under DEP-1, every guard green | `test_lock_installed_toolchain_locks_are_in_the_dep1_set` now requires the exact `--only-group ` command, and `test_each_group_pin_reaches_its_own_lock` ties group → lock from the other direction | +| "The toolchain never enters the runtime/SBOM locks" was **prose only** | `default-groups = ["ci-scanners"]` puts the scanners into `requirements.lock` (98 → 121) **and** into `docker/locks/requirements-core.lock` (41 → 69), the SBOM input, straight past its `--no-dev` — because `--no-dev` disables only the group literally named `dev` | `test_dependency_groups_do_not_leak_into_the_runtime_exports` | +| 5 install sites collapsed onto 3 table rows checked with "≥1" | 4 of the 5 individually deletable at **zero** test cost; the coverage job's failure is silent (`pytest -q --cov` dies on `unrecognized arguments`, `\|\| true` swallows it, diff-coverage reports "skipped" and exits 0) | exact site counts in `LOCK_INSTALLED_TOOLCHAINS`, plus per-**job** resolution in `test_quality_advisory_invariants.py` | +| The bootstrap inventory said **one**, the file had **two** | — | `test_security_yml_pip_bootstrap_count_is_exact`, and the semgrep residual row above | +| The inline-reinstall scan missed `pipx install` and swept only 3 workflows | `pipx install bandit`, or a `pip install bandit` in `ci.yml` | `_PIPX_INSTALL` + a sweep of all 16 workflows | +| `_locked_requirements` silently skipped any non-`--hash` directive | an `--index-url` redirect or an `-e .` line, uncounted | that branch now raises | + +The pattern is the one this ADR's own Decision names: **a claim recorded without a check is a dismissal +with a reason that can quietly become false.** Two false claims were also corrected in place rather than +left standing — the `sigstore` row's tense (see the residuals) and a `security.yml` comment asserting a +bootstrap was gone from a job that still runs one. + +### Adjacent fix folded in + +`.gitattributes` was missing **`constraints.lock`** (`git check-attr text -- constraints.lock` → +`unspecified`), so under `core.autocrlf=true` it checks out CRLF — exactly the drift that stanza exists +to prevent. Harmless so far only because git's clean filter normalizes before `git diff`, which is also +why export sync must be verified with `git diff` and never a raw `diff`. Fixed alongside the new +`ci/locks/*.lock text eol=lf` entry. Closes no alert. + +**Still open, not done:** `quality-advisory.yml`'s `pipx install ruff` fallback installs *unpinned* ruff +instead of failing closed, and `constraints.lock` is `sed`-scraped for a `ruff==` pin that — unlike +`packaging==` — has **no PR-time canary test**. Both are recorded here; neither closes a Scorecard alert. diff --git a/pyproject.toml b/pyproject.toml index 4046bf26..d37f34f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -208,6 +208,82 @@ messagefoundry-tray = "messagefoundry.tray.__main__:main" # and broke `build --wheel`.) tests/test_password_*.py load the corpus via importlib.resources, so a # build-config change that dropped it would fail the suite — that's the guard, not a force-include. +# --- CI TOOLCHAIN — PEP 735 dependency groups (ADR 0034 §3) ---------------------------------------- +# +# WHY THEY EXIST: a version pin does NOT satisfy Scorecard's PinnedDependenciesID. Proven by this +# repo's own alert data — `bandit==1.9.4` (alert #74) and `zizmor==1.5.2` (alert 96) are EXACTLY pinned +# and still flagged, while the two `--require-hashes` installs are flagged in neither the open nor the +# dismissed set. Declaring the tools here routes them through `uv.lock`, out to `ci/locks/*.lock` via +# `uv export` WITH HASHES, and into CI as `pip install --require-hashes -r ci/locks/.lock`. The +# same machinery (DEP-1 diff gate + the Dependabot resync) that keeps the other four exports fresh +# keeps these fresh, which is what stops a hash-pinned toolchain rotting into a pinned-but-unpatched +# one — the failure ADR 0034 §3 names as "worse posture than floating". +# +# NOT EXTRAS, deliberately: an extra is published wheel metadata, so `[ci-scanners]` would become a +# real install target for every downstream consumer of the wheel. A dependency group never ships. +# +# NOT IN `[tool.uv] default-groups`, deliberately: a default group lands in all four committed DEP-1 +# artifacts — i.e. in the release SBOM, the container image locks, and in what `pip-audit` audits AS +# RUNTIME. Measured with these groups non-default: all four re-export byte-identically (DIFFS=0). +# +# THE SPLIT IS THE MERGE PATH. `ci-scanners` is what the BLOCKING security gates install for +# themselves (security.yml's pip-audit + bandit jobs, zizmor.yml); `ci-quality` is ADVISORY +# measurement (quality-advisory.yml's coverage + mutation jobs). Keeping them apart keeps `mutmut` — a +# mutation engine that rewrites and executes source — out of every required gate's install closure. +# +# NOT HERE: `semgrep`. It hard-conflicts with the `[otel]` extra (semgrep 1.172.0 requires +# `opentelemetry-sdk>=1.37,<1.38` while `[otel]` resolves 1.44), so a plain group silently DOWNGRADES +# the shipped otel runtime in all four DEP-1 artifacts — measured, bisected to semgrep alone. The only +# fix is `[tool.uv] conflicts = [[{ extra = "otel" }, { group = "..." }]]`, which declares a PRODUCT +# extra and a CI scanner permanently mutually exclusive. Excluded by decision, recorded as a reasoned +# residual in ADR 0034 §3 with the exact recipe so a future owner can flip it in one commit. +# +# Each pin's rationale lives HERE rather than in the workflow: the workflow comments would otherwise +# push every dismissed Scorecard alert anchored below them onto a new line number (ADR 0034's +# convergence rule). +[dependency-groups] +# The scanners the BLOCKING security gates install for themselves. Exact `==` throughout — for each of +# these the VERSION IS THE CONTRACT of a gate that can red a PR: +# bandit an unpinned 1.9.x upgrade silently changed `# nosec` parsing (1.9.x wants space-separated +# test IDs) and broke a green branch; this is the findings baseline of a blocking gate. +# pip-audit `==` makes the audit reproducible — an unpinned auditor can change its advisory-database +# handling between two runs of the same commit. +# zizmor zizmor.yml's gate asserts a CLEAN baseline, so a newly-added rule reds a green PR for a +# reason unrelated to its diff. Verify the tag at +# https://github.com/zizmorcore/zizmor/releases if an install 404s. +# NB: this group also hash-pins `pip` itself (it arrives as a pip-audit → pip-api dependency), which is +# what let security.yml drop two `pip install --upgrade pip` bootstraps outright rather than pin them. +ci-scanners = [ + "bandit==1.9.4", + "pip-audit==2.10.1", + "zizmor==1.5.2", +] +# The ADVISORY measurement tools (quality-advisory.yml). Exact where something PARSES the tool's +# output, a floor where nothing does: +# diff-cover `==`: the inline-annotation surface depends on this version's +# `--format github-annotations:` and on adjacent-line coalescing inside +# GitHubAnnotationsReportGenerator. +# mutmut `==`: the workflow SHELL-PARSES mutmut's human-readable output (the `N/M` progress +# line, `🎉 N`, `: survived`, `: no tests`) and reconciles two independent derivations +# of the killed count. A reword produces a GREEN receipt off a wrong number — exactly +# the failure class that workflow's liveness job exists for. Must stay 3.x: 2.5.1 +# crashes on Python 3.14 in its pony-ORM cache before generating a single mutant. +# pytest-cov floor only — nothing parses it; it just has to emit a `coverage.xml` diff-cover +# reads. Pinned at the current major so a re-lock cannot regress to 6.x, and left a +# floor so Dependabot can move it without a pyproject edit. +# pytest-timeout DELIBERATELY the identical spec to `[project.optional-dependencies].dev`. mutmut 3 +# always passes `--timeout`/`--timeout-method` to pytest and dies inside +# BadTestExecutionCommandsException without the plugin, so naming it here makes this +# lock self-sufficient instead of depending on the editable `[dev]` install having +# supplied it. Same spec in both places ⇒ uv resolves ONE version and they cannot +# disagree. +ci-quality = [ + "diff-cover==10.4.1", + "mutmut==3.6.0", + "pytest-cov>=7.0", + "pytest-timeout>=2.3", +] + [tool.pytest.ini_options] asyncio_mode = "auto" # Run the whole suite on ONE shared asyncio event loop (tests AND fixtures). pytest-asyncio's default diff --git a/scripts/security/new_dependency_check.py b/scripts/security/new_dependency_check.py index f5971269..8b05afbe 100644 --- a/scripts/security/new_dependency_check.py +++ b/scripts/security/new_dependency_check.py @@ -19,6 +19,12 @@ human remembering". CLAUDE.md section 5 states the rule in prose, and prose is what a session rationalizes past at 2am. +WHAT IT SWEEPS: ``[project].dependencies``, every ``[project.optional-dependencies]`` extra, and every +PEP 735 ``[dependency-groups]`` group. The last was added 2026-07-29 with the hash-pinned CI toolchain +(ADR 0034 section 3): those names are not shipped in the wheel, but they are resolved into ``uv.lock`` +and installed into the runner that executes the BLOCKING security gates, which makes a squatted scanner +name a sharper target than a runtime dependency rather than a softer one. + WHAT IT CHECKS, per declared distribution: * EXISTS -- PyPI serves a project page for the name at all. * HAS RELEASES -- at least one release with files. A registered-but-empty placeholder is the shape @@ -114,14 +120,28 @@ def requirement_name(spec: str) -> str | None: def declared_distributions(pyproject_text: str) -> dict[str, str]: """Every distribution this project declares -> the spec it was declared with. - Covers ``[project].dependencies`` AND every ``[project.optional-dependencies]`` extra, because an - extra is exactly where a niche, plausible-sounding, hallucination-prone name lands. + Covers THREE tables, because a name this gate does not read is a name that enters the tree unvetted: + + * ``[project].dependencies`` -- the runtime closure. + * every ``[project.optional-dependencies]`` extra -- exactly where a niche, plausible-sounding, + hallucination-prone name lands. + * every PEP 735 ``[dependency-groups]`` group -- the CI toolchain (ADR 0034 section 3). These are + not published wheel metadata, but they ARE resolved into ``uv.lock``, hash-exported to + ``ci/locks/*.lock``, and installed into the runner that executes the blocking security gates. A + slopsquatted scanner name is if anything a SHARPER target than a runtime dependency: it runs with + the job's token before any of these controls report. """ data = tomllib.loads(pyproject_text) project = data.get("project") or {} specs: list[str] = list(project.get("dependencies") or []) for extra_specs in (project.get("optional-dependencies") or {}).values(): specs.extend(extra_specs or []) + for group_specs in (data.get("dependency-groups") or {}).values(): + # A PEP 735 entry is EITHER a requirement string OR a `{include-group = "other"}` table. The + # table names another group in this same file, not a distribution, so it has nothing to vet -- + # and passing the dict to requirement_name() would raise. Include-groups are covered anyway: + # every group is iterated here regardless of who includes it. + specs.extend(spec for spec in (group_specs or []) if isinstance(spec, str)) found: dict[str, str] = {} for spec in specs: diff --git a/tests/test_ci_venv_pinning.py b/tests/test_ci_venv_pinning.py index a2921331..34675f86 100644 --- a/tests/test_ci_venv_pinning.py +++ b/tests/test_ci_venv_pinning.py @@ -54,11 +54,50 @@ The gitleaks/trivy downloads are the non-pip half of the same intake, covered by `test_release_asset_downloads_in_blocking_jobs_are_checksum_verified` at the end of this module. + +**Amendment, 2026-07-29 — the CI toolchain moved into a PEP 735 dependency group.** + +A version pin does not satisfy Scorecard's `PinnedDependenciesID`; this repo's own alert data proves it +(`bandit==1.9.4` is alert #74 and `zizmor==1.5.2` is alert 96, both exactly pinned and both flagged, +while the two `--require-hashes` installs are flagged in neither the open nor the dismissed set). So +`bandit`, `pip-audit`, `zizmor`, `mutmut`, `diff-cover` and `pytest-cov` now live in +`pyproject.toml`'s `[dependency-groups]`, flow into `uv.lock`, and are `uv export`ed to +`ci/locks/ci-scanners.lock` / `ci/locks/ci-quality.lock` WITH hashes. The workflows install them with +`pip install --require-hashes -r `. `bandit` and `pip-audit` were consequently dropped from +`RELEASE_PINNED_TOOLS` — deliberately, in this same commit, exactly as that check's own failure +message instructs. + +**The blindness that creates, and what closes it.** `_install_targets()` skips `-r` *and its +argument*, so `pip install --require-hashes -r ci/locks/ci-scanners.lock` names ZERO targets: it +passes every scan in the first half of this module by being invisible to it, not by being verified. +`test_lock_installed_toolchain_*` and `test_*_moved_tool*` below are what actually look at those +lines. In particular `test_lock_installed_toolchain_locks_are_in_the_dep1_set` is the guard against +ADR 0034 §3's stated failure mode — a hand-maintained lock outside the DEP-1 export/resync machinery +rotting into a pinned, stale, *unpatched* toolchain, which is worse posture than floating. + +**What moving those two rows out of `RELEASE_PINNED_TOOLS` cost, and what pays it back.** That table was +the only check that rejected a *floor* (`_PIN_OPS` deliberately excludes `>=`), so its removal left the +`==` requirement on `bandit`/`pip-audit`/`zizmor` enforced by nothing: asserting the name is declared +does not see a floor, and asserting the *exported lock* is `==`-pinned cannot see one either, because +`uv export` writes `bandit==1.9.4` from `bandit>=1.9.4` just as readily. `EXACT_GROUP_PINS` / +`FLOOR_BY_DESIGN` restore it at the declaration, where the decision now lives. Three further holes the +2026-07-29 adversarial pass found and this module now closes, each verified by injecting the regression: +the install-site COUNT (five sites collapsed onto three table rows, so four were individually +deletable), the export SELECTOR (`--group` for `--only-group` pulls the whole project runtime into a +blocking scanner's install closure while staying pinned, hashed and byte-identical under DEP-1), and +group names LEAKING into the four runtime exports (`--no-dev` only filters the group literally named +`dev`, so it is not the protection it looks like). + +`semgrep` is deliberately NOT in a group and stays a version-pinned inline install: it hard-conflicts +with the project's own `[otel]` extra, and the only resolution (`[tool.uv] conflicts`) would declare a +shipped product extra and a CI scanner permanently mutually exclusive. Recorded as a reasoned residual +in ADR 0034 §3, which is why it is still in `RELEASE_PINNED_TOOLS`. """ from __future__ import annotations import re +import tomllib from pathlib import Path import pytest @@ -172,10 +211,14 @@ def test_scratch_venvs_do_not_hide_an_unpinned_pip_fetch(workflow: str) -> None: # The scanners the BLOCKING jobs install for themselves. These run on every `pull_request` and # three of them back required contexts, so they are held to the release rule despite not being on # the release path — see the 2026-07-29 correction in the module docstring. - ("security.yml", "pip-audit"), + # + # `uv` is the resolver that produces every lock this repo commits, so it cannot be hash-locked BY + # that lock (circular by construction) and stays an inline `==` pin. `semgrep` stays inline by + # decision (the `[otel]` conflict — see the amendment). `bandit` and `pip-audit` were HERE until + # 2026-07-29 and are now installed from `ci/locks/ci-scanners.lock` instead; they are checked by + # `LOCK_INSTALLED_TOOLCHAINS` + `MOVED_TO_A_GROUP` below, not by this table. ("security.yml", "uv"), ("security.yml", "semgrep"), - ("security.yml", "bandit"), ) #: `security.yml`'s OWN unpinned installs, registered rather than pinned. Only the `pip` bootstraps @@ -188,8 +231,71 @@ def test_scratch_venvs_do_not_hide_an_unpinned_pip_fetch(workflow: str) -> None: #: `uv` and `pip-audit` were HERE until 2026-07-29 and are now pinned instead; the reasoning that put #: them here — that this file's jobs are schedule/dispatch-only — was factually wrong (it triggers on #: `pull_request` and `pip-audit` is a required context). See the module docstring. +#: +#: TWO `--upgrade pip` bootstraps are left in the file, not one — counted, because an inventory that +#: undercounts is how a residual becomes invisible: the `uv` install in the DEP-1 step, and the +#: `semgrep` install (`python -m pip install --upgrade pip "semgrep==1.172.0"`, whose *semgrep* pin is +#: registered above in `RELEASE_PINNED_TOOLS` while its unpinned `pip` half is registered HERE). The +#: other two went away with their steps rather than being pinned: the `bandit` and `pip-audit` installs +#: are now `pip install --require-hashes -r ci/locks/ci-scanners.lock`, and that lock hash-pins `pip` +#: itself — so a hash-verified pip arrives in the same command that used to fetch an unverified one. SECURITY_YML_ACCEPTED_UNPINNED = frozenset({"pip"}) +#: How many `--upgrade pip` bootstraps `security.yml` is KNOWN to still carry. An exact count, not a +#: floor: `SECURITY_YML_ACCEPTED_UNPINNED` registers the *name* `pip`, so it cannot distinguish two +#: accepted bootstraps from twenty. Both survivors are named in the docstring above and in ADR 0034 §3's +#: residuals table; a third must be a decision, and lowering this number is the receipt for removing one. +SECURITY_YML_PIP_BOOTSTRAPS = 2 + +#: ``(workflow, lock, install sites)`` triples where a CI toolchain is installed from a hash-pinned +#: `uv export`. +#: +#: These lines are INVISIBLE to every scan in the first half of this module: `_install_targets` skips +#: `-r` and its argument, so `pip install --require-hashes -r ` names zero packages and passes by +#: not being looked at. The tests below are the ones that look. +#: +#: THE COUNT IS LOAD-BEARING, not decoration. Five install sites collapse onto three (workflow, lock) +#: pairs — `security.yml` installs `ci-scanners.lock` twice (the pip-audit step and the bandit step) and +#: `quality-advisory.yml` installs `ci-quality.lock` twice (the coverage job and the mutation job). An +#: `assert lines` non-vacuity check is satisfied by ONE surviving line, so deleting either of a pair +#: leaves its job installing nothing while every check here stays green. Measured: with only "≥1", four +#: of the five sites were individually deletable at zero test cost — and the coverage job's failure mode +#: is silent (`pytest -q --cov` dies on `unrecognized arguments`, `|| true` swallows it, and the +#: diff-coverage step reports "skipped" and exits 0). +LOCK_INSTALLED_TOOLCHAINS = ( + ("security.yml", "ci/locks/ci-scanners.lock", 2), + ("zizmor.yml", "ci/locks/ci-scanners.lock", 1), + ("quality-advisory.yml", "ci/locks/ci-quality.lock", 2), +) + +#: Tools that MOVED from an inline `pip install ==` into a PEP 735 dependency group. Two +#: directions need guarding, and neither is visible to anything else: the declaration silently +#: disappearing from `pyproject.toml` while the lock still carries the name as somebody else's +#: transitive, and a future edit re-adding an inline `pip install bandit` beside the lock install. +MOVED_TO_A_GROUP = ("bandit", "pip-audit", "zizmor", "mutmut", "diff-cover", "pytest-cov") + +#: Moved tools whose `[dependency-groups]` spec must be an EXACT `==` pin, and why the exactness is the +#: contract rather than a preference. This exists because moving `bandit` and `pip-audit` out of +#: `RELEASE_PINNED_TOOLS` deleted the ONLY check that rejected a floor: `test_moved_tools_are_declared_*` +#: asserts the NAME is declared, and `test_lock_installed_toolchain_lock_is_pinned_and_hashed` inspects +#: the EXPORTED lock — which `uv export` writes fully `==`-pinned from a `>=` spec just as readily, so it +#: is structurally blind to the difference. Verified: rewriting all three scanner specs to `>=` re-locked, +#: re-exported byte-identically, and passed every guard in this repo. +#: +#: The failure that buys: a floor means Dependabot's weekly `uv` PR moves a blocking gate's version +#: inside `uv.lock` with NO `pyproject.toml` diff to review, `dependabot-lock-resync.yml` re-exports and +#: stages the new lock automatically, and a required gate's findings baseline changes GREEN. +EXACT_GROUP_PINS = ("bandit", "pip-audit", "zizmor", "diff-cover", "mutmut") + +#: The counterpart: moved tools deliberately declared as a FLOOR. Enumerated so "floor by design" and +#: "floor nobody noticed" cannot look the same. +#: +#: `pytest-cov` — nothing parses its output; it only has to emit a `coverage.xml` that `diff-cover` +#: reads, so it is floored at the current major and left free for Dependabot to move. +#: `pytest-timeout` — deliberately the IDENTICAL spec to `[project.optional-dependencies].dev`, so the +#: hashed toolchain install cannot re-point the plugin the coverage run already executes under. +FLOOR_BY_DESIGN = ("pytest-cov", "pytest-timeout") + def _install_targets(line: str) -> list[str]: """The package tokens a ``pip install`` line names — flags, and the arguments of flags that take @@ -202,9 +308,16 @@ def _install_targets(line: str) -> list[str]: match = _PIP_INSTALL.search(line) if match is None: # pragma: no cover - callers filter on the same regex return [] + return _tokens_after(line[match.end() :]) + + +def _tokens_after(rest: str) -> list[str]: + """The install targets in the tail of an install command — flags, and the arguments of flags that + take one, removed. Shared by the ``pip`` and ``pipx`` scans so the two cannot disagree about what + counts as a target.""" targets: list[str] = [] skip_next = False - for tok in line[match.end() :].split(): + for tok in rest.split(): if skip_next: skip_next = False continue @@ -319,6 +432,452 @@ def test_security_yml_unpinned_installs_are_registered() -> None: ) +# --- the CI toolchain installed from a hash-pinned dependency-group lock --------------------------- + + +def _dist_name(spec: str) -> str: + """The distribution name from a PEP 508 requirement string (`"mutmut==3.6.0"` -> `"mutmut"`).""" + match = re.match(r"^[A-Za-z0-9][A-Za-z0-9._-]*", spec.strip()) + return match.group(0) if match else "" + + +def _dependency_groups() -> dict[str, list[str]]: + """``[dependency-groups]`` -> group name -> its requirement STRINGS. + + PEP 735 lets an entry be a ``{include-group = "other"}`` table rather than a requirement string; + those are dropped here (the included group is iterated on its own anyway). + """ + groups = tomllib.loads((_REPO / "pyproject.toml").read_text(encoding="utf-8")).get( + "dependency-groups" + ) + assert groups, ( + "pyproject.toml declares no [dependency-groups] — the CI toolchain lost its source" + ) + return { + name: [spec for spec in (specs or []) if isinstance(spec, str)] + for name, specs in groups.items() + } + + +def _group_spec(package: str) -> str: + """The PEP 508 spec ``package`` is declared with in ``[dependency-groups]``. + + Raises rather than returning a default: "the declaration is gone" and "the declaration is unpinned" + must not produce the same result. + """ + for specs in _dependency_groups().values(): + for spec in specs: + if _dist_name(spec) == package: + return spec.strip() + raise AssertionError(f"{package!r} is not declared in any [dependency-groups] entry") + + +def _project_declared_names() -> set[str]: + """Every distribution declared in ``[project]`` — runtime deps plus every extra. + + A name declared BOTH in `[project]` and in a dependency group (today: `pytest-timeout`, held + deliberately identical to the `dev` extra) legitimately appears in the runtime exports, so the leak + check below must not treat it as a leak. + """ + project = tomllib.loads((_REPO / "pyproject.toml").read_text(encoding="utf-8")).get( + "project", {} + ) + specs: list[str] = list(project.get("dependencies") or []) + for extra_specs in (project.get("optional-dependencies") or {}).values(): + specs.extend(extra_specs or []) + return {_dist_name(spec) for spec in specs if _dist_name(spec)} + + +def _names_target(line: str, package: str) -> bool: + """Does this `pip install` line name ``package`` as a target (bare, or with a version spec)?""" + return any( + target == package or target.startswith(tuple(f"{package}{op}" for op in _SPEC_OPS)) + for target in _install_targets(line) + ) + + +def _locked_requirements(lock: Path) -> list[tuple[str, int]]: + """Each requirement in a `uv export`, paired with how many `--hash=sha256:` lines it OWNS. + + Structural, per requirement, rather than a whole-file substring search. A `"--hash=sha256:" in + text` check passes on a lock carrying 33 pinned packages and ONE hash — which is precisely the + unverified-install shape it is supposed to be catching. (Learned the hard way: that weaker form + stayed green under an injected regression while the six sibling guards all went red.) + + `uv export` emits `name==version \\` followed by indented `--hash=` continuations, then an optional + `# via ...` comment. + + Any OTHER leading-dash directive is a hard failure rather than a skip. `-e .`, `--index-url`, + `--extra-index-url` and `--find-links` all change what pip actually resolves, and a directive that is + neither counted as a requirement nor rejected is invisible to the very guard whose point is not being + fooled by a lock's shape. + """ + out: list[tuple[str, int]] = [] + for raw in lock.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.startswith("--hash="): + if out: # a hash before any requirement would be malformed; ignore rather than crash + name, count = out[-1] + out[-1] = (name, count + 1) + elif line.startswith("-"): + raise AssertionError( + f"{lock.name} carries the directive {line!r}, which is neither a requirement nor a " + f"`--hash=` line. An index redirect or an editable/project line changes what pip " + f"resolves and would otherwise pass through this parser uncounted — decide about it " + f"here rather than letting it be silently ignored." + ) + else: + out.append((line.removesuffix("\\").strip(), 0)) + return out + + +def _exported_names(export: Path) -> list[str]: + """The `name==version` requirement lines of ANY `uv export`, hashed or not. + + Deliberately separate from `_locked_requirements`: that one is strict about directives because it + guards a `--require-hashes` install, whereas `constraints.lock` is legitimately `--no-hashes` and is + only read here for WHICH names it contains. + """ + return [ + line.removesuffix("\\").strip() + for raw in export.read_text(encoding="utf-8").splitlines() + if (line := raw.strip()) and not line.startswith(("#", "-")) + ] + + +@pytest.mark.parametrize(("workflow", "lock", "sites"), LOCK_INSTALLED_TOOLCHAINS) +def test_lock_installed_toolchain_install_is_hash_verified( + workflow: str, lock: str, sites: int +) -> None: + """Installing FROM a hashed lock buys nothing unless `--require-hashes` is actually passed. + + Without the flag pip treats the file as an ordinary requirements list, ignores every `--hash=` + line, and is free to resolve a different artifact — a silently unpinned install that still reads + like a pinned one at a glance. + + The site COUNT is asserted exactly, not as "at least one": see LOCK_INSTALLED_TOOLCHAINS. A `>= 1` + check is satisfied by one survivor of a pair, so it cannot see a job losing its only install. + """ + lines = [ + ln for ln in _code_lines(_WORKFLOWS / workflow) if _PIP_INSTALL.search(ln) and lock in ln + ] + print(f"[ci-venv-pinning] {workflow}: {len(lines)} install site(s) for {lock}") + # Non-vacuity AND completeness: a renamed lock, a restructured step, or a DELETED sibling install + # must fail loudly rather than pass by matching only what is left. + assert len(lines) == sites, ( + f"{workflow} installs {lock} at {len(lines)} site(s), expected {sites}. If a job legitimately " + f"gained or lost its toolchain install, update the count in LOCK_INSTALLED_TOOLCHAINS in the " + f"same commit — a job silently left with no install still passes every other check here, and " + f"in the coverage job it fails INVISIBLY (`|| true` swallows the missing --cov plugin)." + ) + for ln in lines: + assert "--require-hashes" in ln, ( + f"{workflow} installs {lock} WITHOUT --require-hashes: {ln!r}. The hashes in the lock are " + f"inert unless the flag is passed, so this is an unpinned install wearing a lock's clothes." + ) + + +@pytest.mark.parametrize(("workflow", "lock", "sites"), LOCK_INSTALLED_TOOLCHAINS) +def test_lock_installed_toolchain_lock_is_pinned_and_hashed( + workflow: str, lock: str, sites: int +) -> None: + """The lock itself must be fully `==`-pinned AND carry hashes. + + Blocks substituting a `--no-hashes` export (the shape `constraints.lock` legitimately has, because + pip rejects hashes in a `--constraint` file): that would keep the filename, keep the + `--require-hashes` flag, and fail closed at install time instead of silently — but the failure + would first appear in CI rather than here. + """ + path = _REPO / lock + assert path.is_file(), f"{workflow} installs {lock}, which is not committed" + reqs = _locked_requirements(path) + total_hashes = sum(n for _, n in reqs) + print( + f"[ci-venv-pinning] {lock}: {len(reqs)} requirements, {total_hashes} hashes " + f"({sites} install site(s) in {workflow})" + ) + assert reqs, ( + f"{lock} declares no requirements — an empty lock installs nothing and proves nothing" + ) + unpinned = [name for name, _ in reqs if "==" not in name] + assert not unpinned, f"{lock} has requirement lines without an `==` pin: {unpinned}" + # EVERY requirement, not "the file contains a hash somewhere" — see _locked_requirements. + unhashed = [name for name, count in reqs if count == 0] + assert not unhashed, ( + f"{lock} pins these with `==` but carries NO `--hash=` line for them: {unhashed}. Under " + f"`--require-hashes` pip would reject the file outright, so this is a CI break rather than a " + f"silent hole — but a lock exported with `--no-hashes` defeats the whole point of routing the " + f"toolchain through uv.lock (ADR 0034 §3), and it should fail here rather than in the runner." + ) + + +@pytest.mark.parametrize("lock", sorted({lock for _, lock, _ in LOCK_INSTALLED_TOOLCHAINS})) +def test_lock_installed_toolchain_locks_are_in_the_dep1_set(lock: str) -> None: + """Each toolchain lock must be REGENERATED and diff-gated by DEP-1, not hand-maintained. + + This is the guard against ADR 0034 §3's stated failure mode: "a hand-maintained fifth lock outside + that machinery would rot into a pinned, STALE, UNPATCHED toolchain — worse posture than floating". + A hash-pinned toolchain is only an improvement while something keeps it fresh. + + Only the gate half is asserted here; `tests/test_dep1_lock_resync_lockstep.py` then forces the + Dependabot resync to export, short-circuit on, and stage the identical set. + + THE SELECTOR IS PART OF THE CONTRACT, not just the `-o` path. A `.*` between `uv export` and `-o` + accepts `--group` in place of `--only-group`, and `--group` is additive: it emits the PROJECT's whole + closure plus the group. Measured — `uv export --group ci-scanners -o ci/locks/ci-scanners.lock` grows + the lock from 33 to 69 requirements, pulling `fastapi`, `uvicorn`, `hl7`, `httpx` and `aiosqlite` + into what the BLOCKING bandit and zizmor jobs install — and it is fully `==`-pinned and fully hashed, + so it passed every other guard here, passed the resync lockstep, and re-exported byte-identically + under DEP-1. Nothing went red. The group name is also required to MATCH the lock's stem: without it, + `--only-group ci-quality -o ci/locks/ci-scanners.lock` is equally green. + """ + group = Path(lock).stem + body = (_WORKFLOWS / "security.yml").read_text(encoding="utf-8") + pattern = rf"^\s*uv export --only-group {re.escape(group)} --format requirements\.txt -o {re.escape(lock)}\s*$" + assert re.search(pattern, body, re.MULTILINE), ( + f"security.yml's DEP-1 step does not export {lock} with exactly " + f"`uv export --only-group {group} --format requirements.txt -o {lock}`. Either nothing " + f"regenerates it (so the drift gate never compares it, and a stale hash-pinned toolchain is " + f"worse than a floating one), or the SELECTOR changed: `--group` is additive and would pull the " + f"whole project runtime into a scanner's install closure while staying pinned, hashed and " + f"byte-identical under DEP-1." + ) + + +@pytest.mark.parametrize("lock", sorted({lock for _, lock, _ in LOCK_INSTALLED_TOOLCHAINS})) +def test_each_group_pin_reaches_its_own_lock(lock: str) -> None: + """Every spec declared in group `` must appear `==`-pinned in `ci/locks/.lock`. + + The second half of the selector guard, from the other direction: this is what catches a lock exported + from the WRONG group (the filename says `ci-scanners`, the export said `--only-group ci-quality`) and + a pin bumped in `pyproject.toml` without a re-export — which would leave the declared version and the + installed version disagreeing, with `pyproject.toml` reading as the source of truth it no longer is. + """ + group = Path(lock).stem + specs = _dependency_groups().get(group) + assert specs, f"[dependency-groups] has no `{group}` entry, but {lock} is exported from it" + locked = {name.split("==", 1)[0] for name, _ in _locked_requirements(_REPO / lock)} + text = (_REPO / lock).read_text(encoding="utf-8") + print(f"[ci-venv-pinning] {group}: {len(specs)} declared spec(s) checked against {lock}") + for spec in specs: + name = _dist_name(spec) + assert name in locked, ( + f"{lock} does not contain {name!r}, which `[dependency-groups].{group}` declares. The lock " + f"was exported from a different group, or is stale — re-run the DEP-1 exports." + ) + if "==" in spec: + version = spec.split("==", 1)[1].strip().strip("\"'") + assert re.search(rf"^{re.escape(name)}=={re.escape(version)}\b", text, re.MULTILINE), ( + f"`[dependency-groups].{group}` declares {spec!r} but {lock} pins a different version. " + f"A pin bumped without a re-export means pyproject.toml no longer describes what CI " + f"installs — re-run the DEP-1 exports in the same commit." + ) + + +@pytest.mark.parametrize("package", EXACT_GROUP_PINS) +def test_moved_tool_pins_are_exact_not_floors(package: str) -> None: + """A moved tool's `[dependency-groups]` spec must be `==`, not a floor. + + NOTHING ELSE CHECKS THIS. Moving `bandit`/`pip-audit` out of `RELEASE_PINNED_TOOLS` removed the only + guard that rejected a floor (`_PIN_OPS` excludes `>=` by design), and the two replacements are blind + to it: one asserts the NAME is declared, the other inspects the EXPORTED lock — and `uv export` + writes `bandit==1.9.4` from `bandit>=1.9.4` just as readily. Verified end to end: rewriting all three + scanner specs to `>=` re-locked, re-exported the two toolchain locks BYTE-IDENTICALLY, and passed + every guard in this repo. + + Why exactness rather than freshness: each of these versions is the CONTRACT of a gate that can red a + PR — bandit's `# nosec` parsing is a blocking gate's findings baseline, zizmor's gate asserts a clean + baseline so a new rule reds an unrelated PR, and the mutation job SHELL-PARSES mutmut's human-readable + output. A floor lets Dependabot's weekly `uv` PR move any of them with no `pyproject.toml` diff to + review, since the resync re-exports the lock automatically. + """ + spec = _group_spec(package) + assert re.fullmatch(rf"{re.escape(package)}==\d+(\.\d+)*", spec), ( + f"`[dependency-groups]` declares {spec!r}; {package} must be an EXACT `==` pin. `>=` is a FLOOR: " + f"it re-resolves inside uv.lock on any re-lock, and the exported lock looks identically pinned " + f"either way. If a floor is genuinely wanted, move {package!r} to FLOOR_BY_DESIGN with the " + f"reason — do not relax this pattern." + ) + + +@pytest.mark.parametrize("package", FLOOR_BY_DESIGN) +def test_floored_group_pins_stay_declared(package: str) -> None: + """The counterpart to EXACT_GROUP_PINS: a floor is a decision, so it must be an enumerated one. + + Asserts only that the entry still EXISTS and is not accidentally exact-pinned without the reason + moving with it. The point is that reading these two tables together tells you every group spec's + intended shape — so "floor by design" can never be mistaken for "floor nobody noticed". + """ + spec = _group_spec(package) + print(f"[ci-venv-pinning] floor by design: {spec}") + assert re.fullmatch(rf"{re.escape(package)}(>=|~=)\d+(\.\d+)*", spec), ( + f"`{package}` is listed in FLOOR_BY_DESIGN but declared as {spec!r}. If it was deliberately " + f"tightened to an exact pin, move it to EXACT_GROUP_PINS with its reason in the same commit." + ) + + +def test_dependency_groups_do_not_leak_into_the_runtime_exports() -> None: + """No CI-toolchain name may appear in the four PRE-EXISTING DEP-1 artifacts. + + This is the load-bearing invariant `pyproject.toml` asserts in prose and nothing enforced: the groups + must stay NON-DEFAULT, because a default group lands in `requirements.lock`, in the container image + locks that feed BOTH SBOM builds, and in what `pip-audit` audits AS RUNTIME. + + `--no-dev` is NOT the protection it looks like. `uv export --help`: it disables "the development + dependency group" — the group literally named `dev` — so it does not filter any other default group. + Measured: adding `default-groups = ["ci-scanners"]` puts `bandit`, `pip-audit` and `zizmor` into + `requirements.lock` (98 -> 121 requirements) AND into `docker/locks/requirements-core.lock` + (41 -> 69), the SBOM input, past its `--no-dev`. The two `--all-extras` exports pass neither + `--no-dev` nor `--no-default-groups`, so they are unguarded by construction — including against the + most natural mistake available, a group named `dev` mirroring the existing `dev` EXTRA, which is uv's + implicit default group. In every one of those cases the whole suite still passes and DEP-1 merely + demands a re-export, then blesses the leak. + + A name declared in BOTH `[project]` and a group is exempt: `pytest-timeout` is deliberately held to + the identical spec as the `dev` extra, so its presence in the runtime exports is correct. + """ + runtime_exports = ( + "requirements.lock", + "constraints.lock", + "docker/locks/requirements-core.lock", + "docker/locks/requirements-sqlserver.lock", + ) + groups = _dependency_groups() + project_names = _project_declared_names() + toolchain = { + _dist_name(spec) + for specs in groups.values() + for spec in specs + if _dist_name(spec) and _dist_name(spec) not in project_names + } + assert toolchain, "no CI-toolchain-only names to check — this guard would pass vacuously" + print( + f"[ci-venv-pinning] checked {len(runtime_exports)} runtime exports for " + f"{len(toolchain)} toolchain-only name(s): {sorted(toolchain)}" + ) + + leaks: list[str] = [] + for export in runtime_exports: + path = _REPO / export + assert path.is_file(), f"{export} is missing — the DEP-1 artifact set changed" + names = {name.split("==", 1)[0] for name in _exported_names(path)} + print(f"[ci-venv-pinning] {export}: {len(names)} requirements") + leaks += [f"{export}: {name}" for name in sorted(toolchain & names)] + assert not leaks, ( + "a [dependency-groups] name reached a RUNTIME export:\n " + + "\n ".join(leaks) + + "\nThe CI toolchain must stay in NON-DEFAULT groups. Check for a `[tool.uv] default-groups` " + "entry, or a group named `dev` (uv's implicit default group). A leaked name is in the release " + "SBOM, the container image locks, and what pip-audit reports as this project's runtime." + ) + + +@pytest.mark.parametrize("package", MOVED_TO_A_GROUP) +def test_moved_tools_are_declared_in_a_dependency_group(package: str) -> None: + """Each moved tool is still DECLARED in `[dependency-groups]`, not merely present in the lock. + + The direction this catches: the declaration is deleted while the exported lock still carries the + name as some other tool's transitive. The install keeps working, the version stops being ours to + choose, and every other check here still passes. + """ + groups = tomllib.loads((_REPO / "pyproject.toml").read_text(encoding="utf-8")).get( + "dependency-groups" + ) + assert groups, ( + "pyproject.toml declares no [dependency-groups] — the CI toolchain lost its source" + ) + declared = { + _dist_name(spec) + for specs in groups.values() + for spec in specs or [] + # PEP 735 lets an entry be a `{include-group = "..."}` table rather than a requirement string. + if isinstance(spec, str) + } + print(f"[ci-venv-pinning] [dependency-groups] declares: {sorted(declared)}") + assert package in declared, ( + f"{package!r} is installed from a ci/locks lock but is no longer declared in any " + f"[dependency-groups] entry ({sorted(declared)}). It may still resolve as a transitive of " + f"another tool — in which case its version is no longer a decision this repo makes." + ) + + +#: `pipx install ` — the OTHER installer in this repo's workflows, and one `_PIP_INSTALL` cannot +#: see (it anchors on `pip`/`pip3`, so `pipx` never matches). Not hypothetical: `quality-advisory.yml` +#: already installs `ruff` with `pipx` twice, so it is in-repo idiom rather than a shape nobody would +#: reach for. `pipx` has no `--require-hashes`, so a moved tool arriving this way is unpinnable. +_PIPX_INSTALL = re.compile(r"\bpipx\s+(?:-\S+\s+)*install\b") + + +def _pipx_targets(line: str) -> list[str]: + """The package tokens a ``pipx install`` line names — same token walk as `_install_targets`.""" + match = _PIPX_INSTALL.search(line) + if match is None: + return [] + return _tokens_after(line[match.end() :]) + + +def test_no_moved_tool_is_reinstalled_inline() -> None: + """No moved tool may ALSO be installed by an inline `pip install ` or `pipx install `. + + The regression: someone adds `pip install bandit` beside the lock install (to debug, or because a + lock refresh was inconvenient). Both run, the last one wins, and the version that actually executes + is the unpinned one — while every hash-pinning check above still passes, because they only ever + look at the lock line. + + TWO blind spots were closed here rather than left implied. (1) The scan covered only the three + workflows in `LOCK_INSTALLED_TOOLCHAINS`, so `pip install bandit` added to `ci.yml` or `codeql.yml` + was invisible — it now sweeps EVERY workflow. (2) `_PIP_INSTALL` does not match `pipx install`, which + this repo already uses, so that spelling defeated the guard entirely. + """ + workflows = sorted(_WORKFLOWS.glob("*.yml")) + assert workflows, f"no workflows found under {_WORKFLOWS} — this guard would pass vacuously" + print( + f"[ci-venv-pinning] swept {len(workflows)} workflow(s) for inline installs of " + f"{len(MOVED_TO_A_GROUP)} moved tool(s)" + ) + offenders = [ + f"{wf.name}: {package} named inline in {ln!r}" + for wf in workflows + for ln in _code_lines(wf) + if _PIP_INSTALL.search(ln) or _PIPX_INSTALL.search(ln) + for package in MOVED_TO_A_GROUP + if _names_target(ln, package) + or any( + target == package or target.startswith(tuple(f"{package}{op}" for op in _SPEC_OPS)) + for target in _pipx_targets(ln) + ) + ] + assert not offenders, ( + "a tool routed through a hash-pinned dependency group is ALSO installed inline:\n " + + "\n ".join(offenders) + + "\nRemove the inline install; bump the pin in pyproject.toml's [dependency-groups] and " + "re-export instead. (`pipx` has no --require-hashes at all, so that spelling cannot be pinned.)" + ) + + +def test_security_yml_pip_bootstrap_count_is_exact() -> None: + """`security.yml` carries exactly `SECURITY_YML_PIP_BOOTSTRAPS` unpinned `--upgrade pip` fetches. + + `SECURITY_YML_ACCEPTED_UNPINNED` registers the NAME `pip`, so it cannot tell two accepted bootstraps + from twenty — and the inventory in this module claimed ONE while the file carried TWO (the `uv` install + in the DEP-1 step and the `semgrep` install). ADR 0034 §3 is the register a future session reads before + re-dismissing a Scorecard alert, so an inventory that undercounts is how a real finding becomes + invisible. Counted here so the number cannot drift out of the prose again. + """ + lines = [ln for ln in _code_lines(_WORKFLOWS / "security.yml") if "--upgrade pip" in ln] + print(f"[ci-venv-pinning] security.yml --upgrade pip bootstraps: {len(lines)}") + for ln in lines: + print(f"[ci-venv-pinning] {ln}") + assert len(lines) == SECURITY_YML_PIP_BOOTSTRAPS, ( + f"security.yml has {len(lines)} `--upgrade pip` bootstrap(s), expected " + f"{SECURITY_YML_PIP_BOOTSTRAPS}: {lines}. Each surviving one is a Scorecard " + f"PinnedDependenciesID finding that ADR 0034 §3 must name as a residual. Removing one is good " + f"news — lower the constant. ADDING one needs a reason in that residuals table." + ) + + # --- the non-pip half of the same intake: fetched release assets ---------------------------------- diff --git a/tests/test_new_dependency_check.py b/tests/test_new_dependency_check.py index 117718a3..9d408ebc 100644 --- a/tests/test_new_dependency_check.py +++ b/tests/test_new_dependency_check.py @@ -90,6 +90,73 @@ def test_declared_distributions_covers_extras_not_just_core() -> None: assert set(found) == {"fastapi", "hl7", "pynetdicom", "pydicom", "hvac"} +def test_declared_distributions_covers_pep735_dependency_groups() -> None: + """The CI toolchain (ADR 0034 §3) is declared in `[dependency-groups]`, not in an extra. + + Those names are resolved into `uv.lock` and installed into the runner that executes the BLOCKING + security gates, so a squatted scanner name lands with the job's token in hand. A gate that cannot + see the table cannot vet it, and would report a clean sweep while ignoring it entirely. + """ + found = declared_distributions( + """ + [project] + dependencies = ["fastapi>=0.100"] + + [dependency-groups] + ci-scanners = ["bandit==1.9.4", "zizmor==1.5.2"] + ci-quality = ["mutmut==3.6.0", "pytest-cov>=7.0"] + """ + ) + assert set(found) == {"fastapi", "bandit", "zizmor", "mutmut", "pytest-cov"} + + +def test_an_include_group_table_is_skipped_not_crashed_on() -> None: + """PEP 735 lets a group entry be `{include-group = "other"}` instead of a requirement string. + + That table names another GROUP, not a distribution, so there is nothing to vet — and handing the + dict to `requirement_name()` would raise, taking the whole gate down with it. The included group's + own members are still swept, because every group is iterated regardless of who includes it. + """ + found = declared_distributions( + """ + [project] + dependencies = [] + + [dependency-groups] + base = ["bandit==1.9.4"] + everything = [{ include-group = "base" }, "zizmor==1.5.2"] + """ + ) + assert set(found) == {"bandit", "zizmor"} + + +def test_a_bogus_name_declared_in_a_dependency_group_is_rejected() -> None: + """End-to-end: a hallucinated name in a GROUP must fail vetting exactly like one in an extra. + + Drives the real `declared_distributions` -> `vet` path rather than asserting the parser alone, so + "the table is parsed" and "the table is enforced" are two different claims and both are proven. + """ + declared = declared_distributions( + """ + [project] + dependencies = ["fastapi>=0.100"] + + [dependency-groups] + ci-scanners = ["bandit==1.9.4", "hl7-sast-scanner==2.0"] + """ + ) + assert "hl7-sast-scanner" in declared, "the group name never reached the vetting stage" + findings, examined = vet( + declared, + lambda name: None if name == "hl7-sast-scanner" else _fetch_all_good(name), + now=_NOW, + ) + assert examined == 3 + assert [(f.distribution, f.problem) for f in findings] == [ + ("hl7-sast-scanner", "does not exist on PyPI") + ] + + # --- the vetting rules: each failure mode proven, not assumed ---------------------------------- @@ -305,6 +372,39 @@ def test_the_real_pyproject_declares_a_plausible_number_of_distributions() -> No # Spot-check one core dep and one extra, so a table being dropped entirely is caught by name. assert "fastapi" in declared assert "pynetdicom" in declared, "the [dicom] extra is not being read" + assert "bandit" in declared, "the [dependency-groups] CI toolchain is not being read" + + +def test_the_ci_toolchain_groups_actually_raise_the_examined_count() -> None: + """The count must MOVE, not merely be plausible — a parser that silently reads nothing new passes + every floor above. + + Measured: 41 distributions before `[dependency-groups]` was swept, 47 after. The six added names + are the CI toolchain (`bandit`, `pip-audit`, `zizmor`, `diff-cover`, `mutmut`, `pytest-cov`); + `pytest-timeout` is declared in BOTH the `dev` extra and `ci-quality` and so adds nothing, which is + itself deliberate — the identical spec in both places is what stops uv resolving two versions. + + Asserted as a SET DELTA rather than a magic total, so adding an ordinary dependency tomorrow does + not red this, but the group table falling out of the parser does. + """ + from scripts.security.new_dependency_check import declared_distributions as parse + + text = (_ROOT / "pyproject.toml").read_text(encoding="utf-8") + with_groups = set(parse(text)) + + # The same file with [dependency-groups] renamed to an inert table = the pre-change behaviour. + without = set(parse(text.replace("\n[dependency-groups]\n", "\n[inert-not-a-real-table]\n", 1))) + + added = with_groups - without + print(f"examined: {len(without)} -> {len(with_groups)} (+{len(added)}): {sorted(added)}") + assert added == {"bandit", "pip-audit", "zizmor", "diff-cover", "mutmut", "pytest-cov"}, ( + f"the [dependency-groups] sweep added {sorted(added)}; expected the six CI toolchain names. " + "If a tool was deliberately added or removed, re-point this set in the same commit." + ) + assert "pytest-timeout" in without, ( + "pytest-timeout must remain declared in the [dev] extra too — the ci-quality group deliberately " + "repeats its exact spec so uv cannot resolve two different versions" + ) def test_every_finding_message_names_the_remedy() -> None: diff --git a/tests/test_quality_advisory_invariants.py b/tests/test_quality_advisory_invariants.py index 7573fba3..11789f44 100644 --- a/tests/test_quality_advisory_invariants.py +++ b/tests/test_quality_advisory_invariants.py @@ -17,12 +17,43 @@ """ import re +import tomllib from pathlib import Path import pytest import yaml -_WORKFLOW = Path(__file__).resolve().parents[1] / ".github" / "workflows" / "quality-advisory.yml" +_REPO = Path(__file__).resolve().parents[1] +_WORKFLOW = _REPO / ".github" / "workflows" / "quality-advisory.yml" +#: The hash-pinned `uv export` of `[dependency-groups].ci-quality` that this workflow installs from. +_CI_QUALITY_LOCK = _REPO / "ci" / "locks" / "ci-quality.lock" +#: The path as the workflow spells it. +_CI_QUALITY_LOCK_REF = "ci/locks/ci-quality.lock" + + +def _group_pin(package: str) -> str: + """The PEP 508 spec ``package`` is declared with in ``[dependency-groups]``. + + The advisory tools' pins moved out of this workflow and into `pyproject.toml` so they could flow + into `uv.lock` and be exported WITH HASHES (ADR 0034 §3 — a version pin does not satisfy Scorecard's + PinnedDependenciesID). Fails loudly rather than returning a default: "the declaration is gone" and + "the declaration is unpinned" must not produce the same result. + """ + groups = tomllib.loads((_REPO / "pyproject.toml").read_text(encoding="utf-8")).get( + "dependency-groups" + ) + assert groups, "pyproject.toml declares no [dependency-groups]" + for specs in groups.values(): + for spec in specs or []: + # A PEP 735 entry may be a `{include-group = "..."}` table rather than a requirement string. + if isinstance(spec, str) and re.match( + rf"^{re.escape(package)}(?![A-Za-z0-9._-])", spec + ): + return spec + raise AssertionError( + f"{package!r} is not declared in any [dependency-groups] entry: {groups!r}" + ) + # Steps that actually run a quality tool. Setup steps (checkout, setup-python, apt-get, the tool # installs) are deliberately NOT required to be continue-on-error: masking an infrastructure failure @@ -79,6 +110,36 @@ def _steps(workflow: dict) -> list[tuple[str, dict]]: return [(name, step) for name, job in workflow["jobs"].items() for step in job["steps"]] +def _assert_job_installs_the_toolchain_lock(workflow: dict, job: str) -> None: + """The NAMED job must install the hash-pinned toolchain lock — resolved per job, not file-wide. + + A `"ci/locks/ci-quality.lock" in ` assertion is satisfied by EITHER job's install + line, so deleting the coverage job's while leaving the mutation job's (or vice versa) passes. Both + were individually deletable at zero test cost before this. The coverage job's failure mode is the + dangerous one because it is SILENT: without the install, `pytest -q --cov` dies on + `unrecognized arguments: --cov`, `|| true` swallows the exit code, no `coverage.xml` is written, and + the diff-coverage step reports "skipped" and exits 0 — a green advisory receipt measuring nothing. + """ + assert job in workflow["jobs"], ( + f"quality-advisory.yml has no `{job}` job — re-point this guard rather than letting it pass" + ) + runs = [ + step.get("run") or "" + for step in workflow["jobs"][job]["steps"] + if _CI_QUALITY_LOCK_REF in (step.get("run") or "") + ] + print(f"[quality-advisory] job {job!r}: {len(runs)} step(s) install {_CI_QUALITY_LOCK_REF}") + assert len(runs) == 1, ( + f"the `{job}` job has {len(runs)} step(s) installing {_CI_QUALITY_LOCK_REF}, expected exactly 1. " + f"Its tools are pinned by that lock alone; with no install the job runs against whatever the " + f"editable install happened to pull in." + ) + assert "--require-hashes" in runs[0], ( + f"the `{job}` job installs {_CI_QUALITY_LOCK_REF} without --require-hashes — the hashes in the " + f"lock are inert unless the flag is passed." + ) + + def _analysis_steps(workflow: dict) -> list[tuple[str, dict]]: return [ (job, step) @@ -158,9 +219,29 @@ def test_jscpd_stays_on_4x(raw: str) -> None: assert re.search(r"jscpd@4\.\d+\.\d+", raw), "jscpd must stay pinned to a 4.x release" -def test_diff_cover_is_pinned_exactly(raw: str) -> None: - """The annotation surface depends on this version's `--format github-annotations:`.""" - assert re.search(r'"diff-cover==\d+\.\d+\.\d+"', raw), "diff-cover must be pinned with ==" +def test_diff_cover_is_pinned_exactly(workflow: dict) -> None: + """The annotation surface depends on this version's `--format github-annotations:`. + + Re-pointed 2026-07-29: the pin moved OUT of this workflow. `diff-cover` is now declared in + `pyproject.toml`'s `[dependency-groups].ci-quality` and installed from the hash-pinned + `ci/locks/ci-quality.lock`, because a version pin alone does not satisfy Scorecard's + `PinnedDependenciesID` (ADR 0034 §3). So assert the pin where it now lives — in BOTH places, since + an exact spec in pyproject that the exported lock disagrees with would mean the lock is stale. + + Also asserts the COVERAGE job specifically installs that lock. Dropping the `raw` parameter left + nothing per-job checking the coverage side at all: `_group_pin` reads `pyproject.toml`, and the + lock-install assertion in `test_mutmut_*` is workflow-wide. + """ + _assert_job_installs_the_toolchain_lock(workflow, "coverage") + pin = _group_pin("diff-cover") + assert re.fullmatch(r"diff-cover==\d+\.\d+\.\d+", pin), ( + f"diff-cover must be pinned exactly in [dependency-groups], got {pin!r}" + ) + version = pin.split("==", 1)[1] + lock = _CI_QUALITY_LOCK.read_text(encoding="utf-8") + assert re.search(rf"^diff-cover=={re.escape(version)}\b", lock, re.MULTILINE), ( + f"ci/locks/ci-quality.lock does not pin diff-cover=={version} — re-run the DEP-1 exports" + ) def test_the_ruff_version_is_derived_from_the_lock_not_hardcoded(workflow: dict, code: str) -> None: @@ -262,13 +343,32 @@ def test_step_summary_writes_are_size_guarded(code: str) -> None: ) -def test_mutmut_is_pinned_to_3x_with_pytest_timeout(code: str) -> None: +def test_mutmut_is_pinned_to_3x_with_pytest_timeout(code: str, workflow: dict) -> None: """mutmut 2.5.1 crashes on Python 3.14 before generating a mutant, and `|| true` made that look green for months. pytest-timeout is not optional: mutmut 3 always passes `--timeout` to pytest, - and without the plugin every invocation dies inside BadTestExecutionCommandsException.""" - assert re.search(r'"mutmut==3\.\d+\.\d+"', code), "mutmut must be pinned to an exact 3.x" + and without the plugin every invocation dies inside BadTestExecutionCommandsException. + + Re-pointed 2026-07-29 to `[dependency-groups].ci-quality` + `ci/locks/ci-quality.lock` (ADR 0034 + §3). The two BEHAVIOURAL assertions are kept and still asserted against the LOCK, which is what the + job actually installs: `mutmut` must be a 3.x, and `pytest-timeout` must be present. Those are + properties of the installed environment, not of a pin's spelling. + """ + pin = _group_pin("mutmut") + assert re.fullmatch(r"mutmut==3\.\d+\.\d+", pin), ( + f"mutmut must be pinned to an exact 3.x in [dependency-groups], got {pin!r}" + ) assert "mutmut<3" not in code, "mutmut 2.x does not run on Python 3.14" - assert "pytest-timeout" in code, "mutmut 3 requires pytest-timeout" + lock = _CI_QUALITY_LOCK.read_text(encoding="utf-8") + assert re.search(r"^mutmut==3\.", lock, re.MULTILINE), ( + "ci/locks/ci-quality.lock does not resolve mutmut to a 3.x — re-run the DEP-1 exports" + ) + assert re.search(r"^pytest-timeout==", lock, re.MULTILINE), ( + "ci/locks/ci-quality.lock carries no pytest-timeout; mutmut 3 always passes `--timeout` to " + "pytest, so every invocation would die inside BadTestExecutionCommandsException" + ) + # Resolved under `jobs.mutation`, not searched file-wide: the coverage job's install line would + # otherwise satisfy this assertion for the mutation job. + _assert_job_installs_the_toolchain_lock(workflow, "mutation") def test_mutmut_copies_the_package_not_just_the_mutated_file(code: str) -> None: diff --git a/uv.lock b/uv.lock index 0518dbe1..8949b977 100644 --- a/uv.lock +++ b/uv.lock @@ -174,6 +174,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + [[package]] name = "bcrypt" version = "5.0.0" @@ -225,6 +240,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, ] +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, +] + [[package]] name = "cbor2" version = "6.1.2" @@ -291,6 +333,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "chardet" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -353,6 +414,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, +] + [[package]] name = "cryptography" version = "49.0.0" @@ -403,6 +503,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] +[[package]] +name = "cyclonedx-python-lib" +version = "11.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/c9/5d0ccdd19bc7d8ab803b90695c1706aa2ea8529685d18e682dc2524d2630/cyclonedx_python_lib-11.11.0.tar.gz", hash = "sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102", size = 1442983, upload-time = "2026-06-17T11:57:49.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/f3/56ccb2884aaa3db5622368e5191a3384b15f35392aa93df8b2f508c660d2/cyclonedx_python_lib-11.11.0-py3-none-any.whl", hash = "sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44", size = 528689, upload-time = "2026-06-17T11:57:47.358Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -412,6 +527,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] +[[package]] +name = "diff-cover" +version = "10.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "jinja2" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/f9/9f49b333bd03e9bbc1bcc3cde14b4927239195f374cb88e8f4aee57550be/diff_cover-10.4.1.tar.gz", hash = "sha256:0ec566955c9ee7da2f6cc48fa16fac7f97ad1fc4e50a887ffb9cfe5eb1e831df", size = 108279, upload-time = "2026-07-24T03:58:08.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/71/bce893908031195b86a8222ac46ebf689c69b0ca10670c1893c56eb87d77/diff_cover-10.4.1-py3-none-any.whl", hash = "sha256:dc8f2654c485ec4f16e679b5af6e205783cde71185d4ceb8157662dca2d531e9", size = 59875, upload-time = "2026-07-24T03:58:07.163Z" }, +] + [[package]] name = "elementpath" version = "5.1.3" @@ -474,6 +604,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/30/7ac78056464c6fd3062701af46574a779cbb378785d58920e8d8141792f7/fhirpathpy-2.2.3-py3-none-any.whl", hash = "sha256:da223da6fcf5efe4db6c1a19994ecebcf61cabb795103902f105485ad7675a10", size = 232575, upload-time = "2026-06-29T16:56:03.932Z" }, ] +[[package]] +name = "filelock" +version = "3.32.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.0" @@ -623,6 +762,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "ldap3" version = "2.9.1" @@ -635,6 +786,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70", size = 432192, upload-time = "2021-07-18T06:34:12.905Z" }, ] +[[package]] +name = "libcst" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/c0/098e5c91ff1537f00c85a6438b6cb1863d17144680cc91f47c87f104a200/libcst-1.9.0.tar.gz", hash = "sha256:087b58a9afe076bb08e2d726478e1f16cb928d67ffa9092817e033c335de522a", size = 914739, upload-time = "2026-07-29T21:28:43.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/2a/4943c71d90975bc59034a057dea346b365c276f308c1d31f5cf2bd85492d/libcst-1.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eccf4c57d273cdd3fe1c67b72cf9bb1bbd4547aa011824e96ccf5b7136057aa4", size = 2044529, upload-time = "2026-07-29T19:25:11.04Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1168b98c2a0f338be3a44753647baab051feaf6167cc887bf4447f8fd920/libcst-1.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32395244edfe6538e0ea2bf82051d60103d3f54861274805c4fb3745efb70a85", size = 2203519, upload-time = "2026-07-29T19:25:12.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/2eaa697a80f899bcf2245680bbfcc1e478eb3b3328c21d4b2880bdf00dac/libcst-1.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:444e84c76cd035cd2fe136838c1a524d34b08216521f6f5093df8a6f6cfa5799", size = 2255879, upload-time = "2026-07-29T19:25:14.137Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/793b9fd96dd52d202f5f2b88d7e54f05ebed767d1bb3b32395a1817bf6ab/libcst-1.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb5d0946f2b4c6711b5d69fe4f833b364f9e7a1a2b08f88b98619dc18975099a", size = 2269807, upload-time = "2026-07-29T19:25:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/a3/f4/1bc7aaea03971c45e8a885fed9fc1c73176dbbd00b0296a3cde9529bafd6/libcst-1.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45808c03528b3ad40b14095348a918e08d98c47b4a125d631cb78e817c0a5b16", size = 2378171, upload-time = "2026-07-29T19:25:17.289Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f7/bc49e367d2bc8817213594dd52cf6b2da2dbbda90b5382d60653999274ac/libcst-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:568288cdbfe3b4ca3ae4852cb0a439ff053dd54c841bc4995bf2b71238b5de40", size = 2177847, upload-time = "2026-07-29T19:25:19.35Z" }, + { url = "https://files.pythonhosted.org/packages/87/80/4d81577a22e6d535d1a3409f3a1c6903e09036f0e17fc47f92169dbc3501/libcst-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:107593af46945593e7825821793393262bc4fa1d3ea24c3ed487b61269bbbdf8", size = 2058134, upload-time = "2026-07-29T19:25:20.769Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7f/c3f3a0e7a1a2adaa76e815e82a7814d6bfe7d49432276bba652248c68d0b/libcst-1.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f6248cb07444ab9a6733a855737a9febed8b9adca51347019348b09a3ac7dfe9", size = 2036102, upload-time = "2026-07-29T19:25:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/4e/af/2f5543255b2c7d749b966adeccc6bfb1652cf8b425afb913d0f3f025a865/libcst-1.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:496c24e0d3240bc7da45dae543aff3f5b6509978c39262d6b84ed2fb999dded2", size = 2194806, upload-time = "2026-07-29T19:25:24.017Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5b/03f4cddce426d005e208b39ea7b2d456e667cdee0f1891360f0fc8430f20/libcst-1.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ea490fa8540503db5f321f0268becab46eb50f710e8cec8041e241fd66f6874f", size = 2246762, upload-time = "2026-07-29T19:25:25.365Z" }, + { url = "https://files.pythonhosted.org/packages/d8/31/9d5fe1e43dc3dbcc74f70f3e0e73fcdd8d84effc1059d8b45974f0d0d2eb/libcst-1.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50ab94bb2524b419056d4003032b8c67102ac800f8d85b3c4260a01746d94dbb", size = 2259633, upload-time = "2026-07-29T19:25:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2b/6752b28d88c3a19b3bc0b9e1838443b6760d5c862b4f4b37955402659e24/libcst-1.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a2faaf92500d0226358125630f5aab4758e8aad3f2d70a10892ec3c700781a54", size = 2368043, upload-time = "2026-07-29T19:25:28.344Z" }, + { url = "https://files.pythonhosted.org/packages/e2/94/775825b2637f8ab05694b6a4b3802ae6783b4e799f9b58d2400c7e2d4369/libcst-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c7b548512db25af9c2997a95fa731bd6b6928ecbad6c0915d7482d8bb42d34f", size = 2176432, upload-time = "2026-07-29T19:25:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/88ad67427c6fd9db929e087912b0a540e5140e5cb77e7ca4170edaac8531/libcst-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:497d5329345f1f5df84e41b0bbd00204b64a2fd30dfe3cfaeaebca633a31e877", size = 2052931, upload-time = "2026-07-29T19:25:31.397Z" }, +] + [[package]] name = "librt" version = "0.13.0" @@ -670,6 +846,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, ] +[[package]] +name = "license-expression" +version = "30.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "lxml" version = "6.1.1" @@ -714,6 +914,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "messagefoundry" source = { editable = "." } @@ -788,6 +1056,19 @@ xml = [ { name = "xmlschema" }, ] +[package.dev-dependencies] +ci-quality = [ + { name = "diff-cover" }, + { name = "mutmut" }, + { name = "pytest-cov" }, + { name = "pytest-timeout" }, +] +ci-scanners = [ + { name = "bandit" }, + { name = "pip-audit" }, + { name = "zizmor" }, +] + [package.metadata] requires-dist = [ { name = "aioodbc", marker = "extra == 'sqlserver'", specifier = ">=0.5" }, @@ -835,6 +1116,66 @@ requires-dist = [ ] provides-extras = ["dev", "dicom", "fhir", "harness", "otel", "postgres", "sftp", "sqlserver", "vault", "webauthn", "x12", "xml"] +[package.metadata.requires-dev] +ci-quality = [ + { name = "diff-cover", specifier = "==10.4.1" }, + { name = "mutmut", specifier = "==3.6.0" }, + { name = "pytest-cov", specifier = ">=7.0" }, + { name = "pytest-timeout", specifier = ">=2.3" }, +] +ci-scanners = [ + { name = "bandit", specifier = "==1.9.4" }, + { name = "pip-audit", specifier = "==2.10.1" }, + { name = "zizmor", specifier = "==1.5.2" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +] + +[[package]] +name = "mutmut" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "coverage" }, + { name = "libcst" }, + { name = "pytest" }, + { name = "setproctitle" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/b0/ebcae42b90b07756b7aa10c4176835f436332e6c1cb28bc35bae83462382/mutmut-3.6.0.tar.gz", hash = "sha256:bcbd3e4d0d2d4edf3dfb42955417279a8866a3dbbcb87d619f2f3fd0ac7fafda", size = 51538, upload-time = "2026-06-06T07:44:51.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5a/a0caa3f9db407b5d12c311bd4c87aa67fdd6e3f329377149e303108a1c51/mutmut-3.6.0-py3-none-any.whl", hash = "sha256:a9f5b8dcf6cbf9496769d7cf8bdbba37a0ec709ad98f88d103238b62f10bdf37", size = 47770, upload-time = "2026-06-06T07:44:50.038Z" }, +] + [[package]] name = "mypy" version = "2.3.0" @@ -987,6 +1328,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] +[[package]] +name = "packageurl-python" +version = "0.17.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -1020,6 +1370,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "pip" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/96/e6f8e9d9d7b9cc4457092712a7e919c3186aa2c2fa9ffed2c5d29cc947e8/pip-26.2.tar.gz", hash = "sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690", size = 1848845, upload-time = "2026-07-29T21:57:56.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/36/a3aed958d60531cb442b7ab4596cda7b3621cfb916f8ae1d6769795c7dc1/pip-26.2-py3-none-any.whl", hash = "sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad", size = 1816475, upload-time = "2026-07-29T21:57:54.763Z" }, +] + +[[package]] +name = "pip-api" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, +] + +[[package]] +name = "pip-audit" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachecontrol", extra = ["filecache"] }, + { name = "cyclonedx-python-lib" }, + { name = "packaging" }, + { name = "pip-api" }, + { name = "pip-requirements-parser" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "rich" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, +] + +[[package]] +name = "pip-requirements-parser" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1075,6 +1489,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] +[[package]] +name = "py-serializable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, +] + [[package]] name = "pyasn1" version = "0.6.4" @@ -1264,6 +1690,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl", hash = "sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3", size = 56008, upload-time = "2026-06-12T20:28:05.999Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pyside6" version = "6.11.1" @@ -1353,6 +1788,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "pytest-rerunfailures" version = "16.4" @@ -1452,6 +1901,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruff" version = "0.15.22" @@ -1477,6 +1939,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, ] +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, +] + [[package]] name = "shiboken6" version = "6.11.1" @@ -1512,6 +2002,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "sspilib" version = "0.5.0" @@ -1538,6 +2037,68 @@ 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" }, ] +[[package]] +name = "stevedore" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8d/008761f6e1000600e5303db30d05724bdcf3d2d186cbb59fac79b52e39ed/stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7", size = 54463, upload-time = "2026-07-02T11:38:07.43Z" }, +] + +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "tomlkit" version = "0.15.1" @@ -1586,6 +2147,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" @@ -1740,3 +2310,21 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/f0/2431c2c8f77326757 wheels = [ { url = "https://files.pythonhosted.org/packages/5c/fb/8be39dc47b0ed9bbc80f384906eed87fa974c9b377a7a430118c3b405550/xmlschema-4.3.2-py3-none-any.whl", hash = "sha256:cf5c970a30f6ebcb3da35260e694704cc0b6794d8ace46125bb2a95e38ed9307", size = 504613, upload-time = "2026-06-30T06:11:47.462Z" }, ] + +[[package]] +name = "zizmor" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/48/a692769e2bbb62635d55849175849bb9fec2240d0a16e16bd28d8cfe314f/zizmor-1.5.2.tar.gz", hash = "sha256:848f04c0d84b085dfb79c66951404372ebf9b3dd0c73076f1baac83db29e74e4", size = 296921, upload-time = "2025-03-23T15:01:14.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/31/f717638faf223776b239040574f67bfdeb2ef3279673c08a099a525c9f4f/zizmor-1.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:03b27c46d87e96a0acdf78190fbb674c174f67ded396c74dadd64d7c4ecba680", size = 4687915, upload-time = "2025-03-23T15:01:06.138Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0a/0a401630e9f98b8660c20a75cfb4995245c738da90d9bc1c3cd709da8a8a/zizmor-1.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a40feabc2c043aca60f9edbd35a676d97d684af5b06ed61a7752869c79b3be30", size = 4443376, upload-time = "2025-03-23T15:01:04.384Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cf/91527ae1e53e3be260545630e740bd34f5dfa566b23360dea5b07d15e4e5/zizmor-1.5.2-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:9d40780b19da7901423de80ccce083a8c0d2114e0cf6432aa20d8e60d15e97f6", size = 4601870, upload-time = "2025-03-23T15:00:57.752Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/4cdec1dff48ef4bb733344568d9d6a8a82cc655581787866cf38d999001a/zizmor-1.5.2-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:df1326fe9f9ddcbe9d862a97571c848276882d0ceeee39e18fcfc9ec5c66cd7b", size = 4513121, upload-time = "2025-03-23T15:01:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/dc/27/7fbb2e2ad2d33de12b15014b762d39b1529b94bcbfebf55b735e67e00bd4/zizmor-1.5.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:0c5f656106c430748858ae459c41c9eed09a1e01e5f42015d80c8fe34740d173", size = 4840803, upload-time = "2025-03-23T15:01:02.222Z" }, + { url = "https://files.pythonhosted.org/packages/4d/80/6061f5d37cadd185c3e921bc20b8fcf34b262cad009911c50010b7e8d3d5/zizmor-1.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e18b06af4208624ef707e7859b07539870ca26b74a89537e1a6bfba2e44fc9a", size = 4590530, upload-time = "2025-03-23T15:01:08.435Z" }, + { url = "https://files.pythonhosted.org/packages/35/61/777a1ac136d8f256d94f2f269baeb11a6874a9241953fd6694fd4eb5751c/zizmor-1.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:506b355c693d40df01bdf26fd1dfcb41801af7b8c51767d4159efdc2655965c4", size = 4525617, upload-time = "2025-03-23T15:01:10.738Z" }, + { url = "https://files.pythonhosted.org/packages/09/46/8ba5dc116afd6105774ba527d66e7ecf635b2fdf56daddd18ef2933201e6/zizmor-1.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8c65b4dbc9690d3f0f5d9756ae0b5ca8d3dfb4f806d82a5195836a3b613ae996", size = 4914859, upload-time = "2025-03-23T15:01:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/df/0e/9fd2a2e3bbc904878c74a4270aef61be8564f88d68a3dc6ebdc3e0ea2b81/zizmor-1.5.2-py3-none-win32.whl", hash = "sha256:dcef697a88983e7ce6948df1603e2325e2cf5c523828af94615e6c432fa8d98a", size = 3939633, upload-time = "2025-03-23T15:01:18.196Z" }, + { url = "https://files.pythonhosted.org/packages/94/60/837501b8fc475086f5f26c72fa77513cedb5f761744bca809fdc2ee68e6a/zizmor-1.5.2-py3-none-win_amd64.whl", hash = "sha256:86395dd985ed6bf9acffea8a900b30d8d4dd4c9e20421f16f2db7ad92299e24e", size = 4435978, upload-time = "2025-03-23T15:01:16.284Z" }, +]