From b4bc82bac318472c7197a8e57f17d8b8d6fd4810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sat, 24 Oct 2026 00:43:51 -0700 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20fix(marketplace):=20block=20?= =?UTF-8?q?ambiguous=20retries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marketplace can accept an upload before the workflow records its result. Check prior attempts and signed artifacts before retrying; require manual reconciliation when acceptance is uncertain. --- README.md | 65 ++- changelog.d/+marketplace-retry.breaking.md | 3 + publish-jetbrains-marketplace/action.yml | 178 +++++++- tests/conftest.py | 2 + .../test_marketplace_upload.py | 412 +++++++++++++++++- 5 files changed, 620 insertions(+), 40 deletions(-) create mode 100644 changelog.d/+marketplace-retry.breaking.md diff --git a/README.md b/README.md index 11c7a4c..a49e2b8 100644 --- a/README.md +++ b/README.md @@ -667,25 +667,60 @@ artifact after the Action succeeds. Keep the Marketplace token in a different no `publish-jetbrains-marketplace` uploads one signed ZIP through the [Marketplace upload API](https://plugins.jetbrains.com/docs/marketplace/plugin-upload.html). Run it in a fresh job -without checkout or Gradle. Download the ZIP produced by the signing job and pass that job's SHA256 output; the Action -checks the downloaded bytes, ZIP integrity, and member names before the token-bearing upload step. +without checkout or Gradle. The release workflow must serialize runs for the same tag. Download the signed ZIP and +release notes by the artifact IDs from their producing jobs. The Action checks both before using the upload token. ```yaml - - uses: tox-dev/github-workflows/publish-jetbrains-marketplace@SHA - env: - PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - with: - archive: dist/Fish Shell 2-0.1.11-signed.zip - sha256: ${{ needs.sign.outputs.sha256 }} - plugin-id: '29973' - channel: '' +marketplace-publish: + needs: [sign, notes] + runs-on: ubuntu-26.04 + permissions: + actions: read + environment: release + steps: + - uses: actions/download-artifact@DOWNLOAD_SHA + with: + artifact-ids: ${{ needs.sign.outputs.artifact-id }} + path: dist + - uses: actions/download-artifact@DOWNLOAD_SHA + with: + artifact-ids: ${{ needs.notes.outputs.artifact-id }} + path: ${{ runner.temp }}/notes + - name: Publish signed ZIP + uses: tox-dev/github-workflows/publish-jetbrains-marketplace@SHA + with: + archive: dist/Plugin-1.2.3-signed.zip + sha256: ${{ needs.sign.outputs.sha256 }} + plugin-id: '29973' + channel: '' + tag: ${{ github.ref_name }} + commit: ${{ github.sha }} + signed-artifact-id: ${{ needs.sign.outputs.artifact-id }} + notes-artifact-id: ${{ needs.notes.outputs.artifact-id }} + notes-file: ${{ runner.temp }}/notes/release-notes.md + github-token: ${{ github.token }} + publish-token: ${{ secrets.PUBLISH_TOKEN }} ``` -`archive`, `sha256`, and numeric `plugin-id` are required. `channel` is empty for Stable; use a Marketplace channel name -for a prerelease. **Output:** None. **Remote effect:** One multipart upload to JetBrains Marketplace. The Action does -not check whether this version was published already; use the keyless release-creator `marketplace --mode stage` check -before the upload, and `--mode verify` afterward. A retry must compare the published ZIP's member digests before -skipping an upload because Marketplace can repackage the outer ZIP. +`channel` is empty for Stable; use a Marketplace channel name for a prerelease. Keep the publishing step named +`Publish signed ZIP`, since the retry check reads that step from earlier workflow runs. Give the job Actions read access +and the workflow a concurrency group keyed by tag. The Action verifies that the remote tag still targets the release +commit, then searches earlier runs for the tag. A prior upload from another commit stops for manual reconciliation. For +the same commit, an acceptance receipt must match the original artifact IDs, signed ZIP digest, and release notes bytes +before the Action skips upload. If an earlier attempt reached the upload step without a receipt, it also stops; +Marketplace may have accepted the upload before the run failed. + +**Output:** A `marketplace-acceptance--` Actions artifact after a new upload. It records the tag, commit, +plugin, channel, signed ZIP artifact and SHA256, and notes artifact and SHA256. **Remote effect:** One multipart upload +to JetBrains Marketplace when no earlier acceptance exists, plus a GitHub Actions receipt artifact. The Action treats +any successful HTTP response as acceptance; it does not depend on undocumented response fields. Run the keyless +release-creator `marketplace --mode stage` check before this job and `--mode verify` after the version is public. + +If the Action stops without a receipt, inspect the exact plugin, version, and channel in the Marketplace dashboard. If +the update is pending review, wait until it is public and verify its member bytes against the original signed ZIP. Then +rerun the tag workflow: the keyless stage check must skip Marketplace upload before GitHub Release publication. If +Marketplace rejected the update, resolve the rejection and prepare a new release tag. A 404 alone does not authorize +another upload of the same tag. ## Test a caller diff --git a/changelog.d/+marketplace-retry.breaking.md b/changelog.d/+marketplace-retry.breaking.md new file mode 100644 index 0000000..2ae92b1 --- /dev/null +++ b/changelog.d/+marketplace-retry.breaking.md @@ -0,0 +1,3 @@ +Require a release tag, source commit, signed ZIP artifact, and release notes artifact when publishing a JetBrains +plugin. The Action now records an acceptance receipt and blocks an ambiguous retry until a maintainer reconciles the +Marketplace update. diff --git a/publish-jetbrains-marketplace/action.yml b/publish-jetbrains-marketplace/action.yml index d836708..e027ba4 100644 --- a/publish-jetbrains-marketplace/action.yml +++ b/publish-jetbrains-marketplace/action.yml @@ -1,5 +1,5 @@ name: Publish JetBrains Marketplace plugin -description: Upload one verified signed ZIP without checking out or building source +description: Upload a signed ZIP once per tag and retain an acceptance receipt inputs: archive: description: Signed plugin ZIP from the validated build artifact @@ -13,6 +13,27 @@ inputs: channel: description: Marketplace channel; empty selects Stable default: "" + tag: + description: Current release tag + required: true + commit: + description: Commit targeted by the tag + required: true + signed-artifact-id: + description: GitHub artifact ID containing the signed ZIP + required: true + notes-artifact-id: + description: GitHub artifact ID containing the release notes + required: true + notes-file: + description: Release notes downloaded from notes-artifact-id + required: true + github-token: + description: Token with Actions read permission + required: true + publish-token: + description: JetBrains Marketplace upload token + required: true runs: using: composite steps: @@ -32,13 +53,129 @@ runs: test "$(sha256sum "$ARCHIVE" | cut -d' ' -f1)" = "$EXPECTED_SHA" unzip -tq "$ARCHIVE" >/dev/null test -z "$(zipinfo -1 "$ARCHIVE" | LC_ALL=C sort | uniq -d)" + - name: Verify release identity + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + TAG: ${{ inputs.tag }} + COMMIT: ${{ inputs.commit }} + SIGNED_ARTIFACT_ID: ${{ inputs.signed-artifact-id }} + NOTES_ARTIFACT_ID: ${{ inputs.notes-artifact-id }} + NOTES_FILE: ${{ inputs.notes-file }} + run: | + set -euo pipefail + [[ "$GITHUB_REF" == "refs/tags/$TAG" && "$COMMIT" == "$GITHUB_SHA" ]] || exit 1 + [[ -f "$NOTES_FILE" && ! -L "$NOTES_FILE" ]] || exit 1 + for artifact_id in "$SIGNED_ARTIFACT_ID" "$NOTES_ARTIFACT_ID"; do + case "$artifact_id" in '' | *[!0-9]*) exit 1 ;; esac + done + target=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --jq '.object | [.type, .sha] | @tsv') + while true; do + read -r object_type object_sha <<<"$target" + if [[ "$object_type" == commit ]]; then + [[ "$object_sha" == "$COMMIT" ]] || exit 1 + break + fi + [[ "$object_type" == tag ]] || exit 1 + target=$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" --jq '.object | [.type, .sha] | @tsv') + done + - name: Find earlier Marketplace upload + id: earlier + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + TAG: ${{ inputs.tag }} + COMMIT: ${{ inputs.commit }} + run: | + set -euo pipefail + workflow_ref=${GITHUB_WORKFLOW_REF%@*} + workflow_file=${workflow_ref##*/} + gh api --paginate "repos/$GITHUB_REPOSITORY/actions/workflows/$workflow_file/runs?per_page=100" \ + --jq '.workflow_runs[] | [.id, .head_branch, .head_sha] | @tsv' >"$RUNNER_TEMP/marketplace-runs.tsv" + prior=0 + while IFS=$'\t' read -r run branch sha; do + [[ "$branch" == "$TAG" ]] || continue + gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs/$run/jobs?filter=all&per_page=100" \ + --jq ".jobs[] | .run_attempt as \$attempt | .steps[] | + select((.name == \"Publish signed ZIP\" or .name == \"Upload signed ZIP\" or + .name == \"📤 Publish to JetBrains Marketplace\") and .started_at != null) | \$attempt" \ + >"$RUNNER_TEMP/marketplace-attempts.txt" + while IFS= read -r attempt; do + if [[ "$run" == "$GITHUB_RUN_ID" && "$attempt" -ge "$GITHUB_RUN_ATTEMPT" ]]; then continue; fi + if [[ "$sha" != "$COMMIT" ]]; then + echo "::error::Tag $TAG had an earlier Marketplace upload on commit $sha. Reconcile run $run attempt $attempt manually." >&2 + exit 1 + fi + gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs/$run/artifacts?per_page=100" \ + --jq '.artifacts[] | [.name, .id, .expired] | @tsv' >"$RUNNER_TEMP/marketplace-artifacts-$run.tsv" + receipt_name="marketplace-acceptance-$run-$attempt" + receipt_id=$(awk -F '\t' -v name="$receipt_name" '$1 == name && $3 == "false" { print $2 }' \ + "$RUNNER_TEMP/marketplace-artifacts-$run.tsv") + if [[ -z "$receipt_id" || "$receipt_id" == *$'\n'* || "$prior" == 1 ]]; then + echo "::error::Marketplace upload may already be accepted. Reconcile run $run attempt $attempt manually." >&2 + exit 1 + fi + prior=1 + { + echo "run=$run" + echo "receipt-name=$receipt_name" + echo "artifacts=$RUNNER_TEMP/marketplace-artifacts-$run.tsv" + } >>"$GITHUB_OUTPUT" + done <"$RUNNER_TEMP/marketplace-attempts.txt" + done <"$RUNNER_TEMP/marketplace-runs.tsv" + echo "accepted=$prior" >>"$GITHUB_OUTPUT" + - name: Verify earlier Marketplace acceptance + if: steps.earlier.outputs.accepted == '1' + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + TAG: ${{ inputs.tag }} + COMMIT: ${{ inputs.commit }} + PLUGIN_ID: ${{ inputs.plugin-id }} + CHANNEL: ${{ inputs.channel }} + ARCHIVE: ${{ inputs.archive }} + EXPECTED_SHA: ${{ inputs.sha256 }} + NOTES_FILE: ${{ inputs.notes-file }} + RUN: ${{ steps.earlier.outputs.run }} + RECEIPT_NAME: ${{ steps.earlier.outputs.receipt-name }} + ARTIFACTS: ${{ steps.earlier.outputs.artifacts }} + run: | + set -euo pipefail + mkdir "$RUNNER_TEMP/prior-acceptance" "$RUNNER_TEMP/prior-signed" "$RUNNER_TEMP/prior-notes" + gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name "$RECEIPT_NAME" \ + --dir "$RUNNER_TEMP/prior-acceptance" + receipt="$RUNNER_TEMP/prior-acceptance/marketplace-acceptance.json" + [[ -f "$receipt" && ! -L "$receipt" ]] || exit 1 + jq -e --arg repository "$GITHUB_REPOSITORY" --arg tag "$TAG" --arg commit "$COMMIT" \ + --arg version "${TAG#v}" --argjson runId "$RUN" --argjson pluginId "$PLUGIN_ID" \ + --arg channel "$CHANNEL" --arg archive "${ARCHIVE##*/}" --arg sha "$EXPECTED_SHA" \ + --arg notesSha "$(sha256sum "$NOTES_FILE" | cut -d' ' -f1)" \ + '.schema == 2 and .repository == $repository and .tag == $tag and .commit == $commit and + .sourceRunId == $runId and .pluginId == $pluginId and .version == $version and + .channel == $channel and .signedArchive == $archive and .sha256 == $sha and + .notesSha256 == $notesSha and (.signedArtifactId | type == "number") and + (.notesArtifactId | type == "number")' "$receipt" >/dev/null + signed_id=$(jq -r .signedArtifactId "$receipt") + notes_id=$(jq -r .notesArtifactId "$receipt") + [[ "$(awk -F '\t' '$1 == "plugin-distribution" && $3 == "false" { print $2 }' "$ARTIFACTS")" == "$signed_id" ]] || exit 1 + [[ "$(awk -F '\t' '$1 == "release-notes" && $3 == "false" { print $2 }' "$ARTIFACTS")" == "$notes_id" ]] || exit 1 + gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name plugin-distribution \ + --dir "$RUNNER_TEMP/prior-signed" + gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name release-notes \ + --dir "$RUNNER_TEMP/prior-notes" + [[ -f "$RUNNER_TEMP/prior-signed/${ARCHIVE##*/}" && ! -L "$RUNNER_TEMP/prior-signed/${ARCHIVE##*/}" ]] || exit 1 + [[ -f "$RUNNER_TEMP/prior-notes/release-notes.md" && ! -L "$RUNNER_TEMP/prior-notes/release-notes.md" ]] || exit 1 + test "$(sha256sum "$RUNNER_TEMP/prior-signed/${ARCHIVE##*/}" | cut -d' ' -f1)" = "$EXPECTED_SHA" + cmp "$NOTES_FILE" "$RUNNER_TEMP/prior-notes/release-notes.md" - name: Upload signed ZIP + if: steps.earlier.outputs.accepted == '0' shell: bash env: ARCHIVE: ${{ inputs.archive }} EXPECTED_SHA: ${{ inputs.sha256 }} PLUGIN_ID: ${{ inputs.plugin-id }} CHANNEL: ${{ inputs.channel }} + PUBLISH_TOKEN: ${{ inputs.publish-token }} run: | set -euo pipefail test -n "$PUBLISH_TOKEN" @@ -46,11 +183,46 @@ runs: cp -- "$ARCHIVE" "$upload_file" test "$(sha256sum "$upload_file" | cut -d' ' -f1)" = "$EXPECTED_SHA" cd "$RUNNER_TEMP" - if ! curl --fail-with-body --max-time 120 --silent --show-error \ + status=$(curl --fail-with-body --max-time 120 --silent --show-error --write-out '%{http_code}' \ --output "$RUNNER_TEMP/marketplace-response.json" \ --config <(printf 'header = "Authorization: Bearer %s"\n' "$PUBLISH_TOKEN") \ --form "pluginId=$PLUGIN_ID" --form "file=@${upload_file##*/};filename=plugin.zip" --form "channel=$CHANNEL" \ - https://plugins.jetbrains.com/api/updates/upload; then + https://plugins.jetbrains.com/api/updates/upload) || { + cat "$RUNNER_TEMP/marketplace-response.json" >&2 + exit 1 + } + if [[ "$status" != 2?? ]]; then cat "$RUNNER_TEMP/marketplace-response.json" >&2 exit 1 fi + - name: Record Marketplace acceptance + if: steps.earlier.outputs.accepted == '0' + shell: bash + env: + TAG: ${{ inputs.tag }} + COMMIT: ${{ inputs.commit }} + PLUGIN_ID: ${{ inputs.plugin-id }} + CHANNEL: ${{ inputs.channel }} + ARCHIVE: ${{ inputs.archive }} + EXPECTED_SHA: ${{ inputs.sha256 }} + SIGNED_ARTIFACT_ID: ${{ inputs.signed-artifact-id }} + NOTES_ARTIFACT_ID: ${{ inputs.notes-artifact-id }} + NOTES_FILE: ${{ inputs.notes-file }} + run: | + set -euo pipefail + jq -n --arg repository "$GITHUB_REPOSITORY" --arg tag "$TAG" --arg commit "$COMMIT" \ + --arg version "${TAG#v}" --argjson runId "$GITHUB_RUN_ID" --argjson pluginId "$PLUGIN_ID" \ + --arg channel "$CHANNEL" --arg archive "${ARCHIVE##*/}" --arg sha "$EXPECTED_SHA" \ + --arg notesSha "$(sha256sum "$NOTES_FILE" | cut -d' ' -f1)" \ + --argjson signedArtifactId "$SIGNED_ARTIFACT_ID" --argjson notesArtifactId "$NOTES_ARTIFACT_ID" \ + '{schema: 2, repository: $repository, tag: $tag, commit: $commit, sourceRunId: $runId, + pluginId: $pluginId, version: $version, channel: $channel, signedArchive: $archive, + sha256: $sha, signedArtifactId: $signedArtifactId, notesArtifactId: $notesArtifactId, + notesSha256: $notesSha}' >"$RUNNER_TEMP/marketplace-acceptance.json" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: steps.earlier.outputs.accepted == '0' + with: + name: marketplace-acceptance-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/marketplace-acceptance.json + if-no-files-found: error + retention-days: 30 diff --git a/tests/conftest.py b/tests/conftest.py index dbf1c1d..d5f221e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -428,6 +428,8 @@ def _run_action( if (used := step.get("uses")) is not None: if used.startswith("astral-sh/setup-uv@"): continue + if used.startswith("actions/upload-artifact@"): + continue if not used.startswith("$/"): msg = f"unsupported nested action: {used}" raise ValueError(msg) diff --git a/tests/publish_jetbrains_marketplace/test_marketplace_upload.py b/tests/publish_jetbrains_marketplace/test_marketplace_upload.py index e7a443a..8c625a0 100644 --- a/tests/publish_jetbrains_marketplace/test_marketplace_upload.py +++ b/tests/publish_jetbrains_marketplace/test_marketplace_upload.py @@ -1,11 +1,15 @@ from __future__ import annotations +import json import shutil +import subprocess from hashlib import sha256 +from io import BytesIO from pathlib import Path from zipfile import ZIP_DEFLATED, ZipFile import pytest +import yaml from conftest import action from pytest_httpserver import HTTPServer @@ -23,7 +27,12 @@ ], ) def test_invalid_archive_or_input_fails_before_upload( - signed_archive: Path, tmp_path: Path, marketplace_scripts: Path, httpserver: HTTPServer, changed: str + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, + changed: str, ) -> None: settings = { "ARCHIVE": str(signed_archive), @@ -52,20 +61,23 @@ def test_invalid_archive_or_input_fails_before_upload( link = tmp_path / "linked.zip" link.symlink_to(signed_archive) settings["ARCHIVE"] = str(link) - result = action( - tmp_path, + result = publish_action( tmp_path, marketplace_scripts, - "publish-jetbrains-marketplace", - version="", + github_api, PUBLISH_TOKEN="fixture-token", - **settings, + ARCHIVE=settings["ARCHIVE"], + SHA256=settings["SHA256"], + PLUGIN_ID=settings["PLUGIN_ID"], + CHANNEL=settings["CHANNEL"], ) assert result.returncode != 0 assert not httpserver.log -@pytest.mark.parametrize("status", [pytest.param(200, id="accepted"), pytest.param(403, id="denied")]) +@pytest.mark.parametrize( + "status", [pytest.param(200, id="accepted"), pytest.param(302, id="redirected"), pytest.param(403, id="denied")] +) @pytest.mark.parametrize( "archive_name", ["Fish Shell 2-0.1.11-signed.zip", "Fish Shell, 2; 0.1.11-signed.zip"], @@ -76,6 +88,7 @@ def test_multipart_upload_uses_exact_archive_and_channel( signed_archive: Path, tmp_path: Path, marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], httpserver: HTTPServer, status: int, archive_name: str, @@ -84,15 +97,13 @@ def test_multipart_upload_uses_exact_archive_and_channel( signed_archive = signed_archive.rename(tmp_path / archive_name) runner_temp = tmp_path / temporary_name runner_temp.mkdir() - httpserver.expect_request("/api/updates/upload", method="POST").respond_with_data( - "upload denied" if status == 403 else "{}", status=status - ) - result = action( + response = {200: "{}", 302: "redirect", 403: "upload denied"}[status] + httpserver.expect_request("/api/updates/upload", method="POST").respond_with_data(response, status=status) + result = publish_action( tmp_path, - runner_temp, marketplace_scripts, - "publish-jetbrains-marketplace", - version="", + github_api, + runner_temp=runner_temp, ARCHIVE=str(signed_archive), SHA256=sha256(signed_archive.read_bytes()).hexdigest(), PLUGIN_ID="29973", @@ -101,8 +112,24 @@ def test_multipart_upload_uses_exact_archive_and_channel( RUNNER_TEMP=str(runner_temp), ) assert (result.returncode == 0) is (status == 200) - if status == 403: - assert "upload denied" in result.stderr + if status != 200: + assert response in result.stderr + else: + assert json.loads((runner_temp / "marketplace-acceptance.json").read_text()) == { + "schema": 2, + "repository": "tox-dev/fixture", + "tag": "v0.1.11", + "commit": "0" * 40, + "sourceRunId": 42, + "pluginId": 29973, + "version": "0.1.11", + "channel": "beta", + "signedArchive": archive_name, + "sha256": sha256(signed_archive.read_bytes()).hexdigest(), + "signedArtifactId": 51, + "notesArtifactId": 52, + "notesSha256": sha256(b"Release notes\n").hexdigest(), + } request = httpserver.log[0][0] assert request.headers["Authorization"] == "Bearer fixture-token" assert request.form["pluginId"] == "29973" @@ -112,14 +139,16 @@ def test_multipart_upload_uses_exact_archive_and_channel( def test_missing_token_fails_without_request( - signed_archive: Path, tmp_path: Path, marketplace_scripts: Path, httpserver: HTTPServer + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, ) -> None: - result = action( - tmp_path, + result = publish_action( tmp_path, marketplace_scripts, - "publish-jetbrains-marketplace", - version="", + github_api, ARCHIVE=str(signed_archive), SHA256=sha256(signed_archive.read_bytes()).hexdigest(), PLUGIN_ID="29973", @@ -131,6 +160,341 @@ def test_missing_token_fails_without_request( assert not httpserver.log +def test_prior_upload_without_receipt_blocks_retry( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_run=True, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "Reconcile run 41 attempt 1 manually" in result.stderr + assert not httpserver.log + + +@pytest.mark.parametrize( + "previous_step", + [ + pytest.param("Publish signed ZIP", id="current"), + pytest.param("📤 Publish to JetBrains Marketplace", id="legacy"), + ], +) +def test_prior_upload_on_another_commit_blocks_retry( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, + previous_step: str, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_run=True, + previous_commit="1" * 40, + previous_step=previous_step, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "earlier Marketplace upload on commit" in result.stderr + assert any( + request.path == "/api/v3/repos/tox-dev/fixture/actions/workflows/release.yaml/runs" + and request.query_string == b"per_page=100" + for request, _ in github_api[0].log + ) + assert not httpserver.log + + +@pytest.mark.parametrize("annotated", [pytest.param(False, id="lightweight"), pytest.param(True, id="annotated")]) +def test_remote_moved_tag_blocks_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, + annotated: bool, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + remote_commit="1" * 40, + annotated=annotated, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert not httpserver.log + + +def test_annotated_tag_targets_release_commit( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + httpserver.expect_request("/api/updates/upload", method="POST").respond_with_data("{}") + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + annotated=True, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode == 0, result.stderr + assert len(httpserver.log) == 1 + + +def test_prior_acceptance_with_matching_artifacts_skips_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_run=True, + accepted_archive=signed_archive, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode == 0, result.stderr + assert not httpserver.log + + +def test_prior_acceptance_missing_receipt_file_blocks_retry( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_run=True, + accepted_archive=signed_archive, + missing_receipt=True, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert not httpserver.log + + +@pytest.mark.parametrize( + ("field", "value"), + [ + pytest.param("tag", "v0.1.12", id="tag"), + pytest.param("commit", "1" * 40, id="commit"), + pytest.param("pluginId", 20536, id="plugin"), + pytest.param("channel", "alpha", id="channel"), + pytest.param("sha256", "0" * 64, id="signed-digest"), + pytest.param("signedArtifactId", 70, id="signed-artifact"), + pytest.param("notesArtifactId", 71, id="notes-artifact"), + pytest.param("notesSha256", "0" * 64, id="notes-digest"), + ], +) +def test_changed_prior_receipt_blocks_retry( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, + field: str, + value: str | int, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_run=True, + accepted_archive=signed_archive, + receipt_changes={field: value}, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert not httpserver.log + + +@pytest.mark.parametrize("changed", [pytest.param("signed", id="signed-zip"), pytest.param("notes", id="notes")]) +def test_changed_prior_artifact_blocks_retry( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, + changed: str, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_run=True, + accepted_archive=signed_archive, + prior_signed=b"changed" if changed == "signed" else None, + prior_notes=b"changed" if changed == "notes" else None, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert not httpserver.log + + +def publish_action( + repository: Path, + scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + *, + runner_temp: Path | None = None, + previous_run: bool = False, + previous_commit: str = "0" * 40, + previous_step: str = "Publish signed ZIP", + accepted_archive: Path | None = None, + missing_receipt: bool = False, + receipt_changes: dict[str, str | int] | None = None, + prior_signed: bytes | None = None, + prior_notes: bytes | None = None, + remote_commit: str = "0" * 40, + annotated: bool = False, + **settings: str, +) -> subprocess.CompletedProcess[str]: + server, environment = github_api + server.expect_request("/api/v3/repos/tox-dev/fixture/git/ref/tags/v0.1.11").respond_with_json( + {"object": {"type": "tag" if annotated else "commit", "sha": "f" * 40 if annotated else remote_commit}} + ) + if annotated: + server.expect_request(f"/api/v3/repos/tox-dev/fixture/git/tags/{'f' * 40}").respond_with_json( + {"object": {"type": "commit", "sha": remote_commit}} + ) + server.expect_request("/api/v3/repos/tox-dev/fixture/actions/workflows/release.yaml/runs").respond_with_json( + { + "workflow_runs": [ + {"id": 41, "head_branch": "v0.1.11", "head_sha": previous_commit}, + ] + if previous_run + else [] + } + ) + if previous_run: + server.expect_request("/api/v3/repos/tox-dev/fixture/actions/runs/41/jobs").respond_with_json( + {"jobs": [{"run_attempt": 1, "steps": [{"name": previous_step, "started_at": "2026-10-23T00:00:00Z"}]}]} + ) + artifacts = ( + [ + { + "id": artifact_id, + "name": artifact_name, + "expired": False, + "archive_download_url": f"https://localhost:{server.port}/api/v3/repos/tox-dev/fixture/actions/artifacts/{artifact_id}/zip", + } + for artifact_id, artifact_name in ( + (61, "marketplace-acceptance-41-1"), + (51, "plugin-distribution"), + (52, "release-notes"), + ) + ] + if accepted_archive + else [] + ) + server.expect_request("/api/v3/repos/tox-dev/fixture/actions/runs/41/artifacts").respond_with_json( + {"artifacts": artifacts} + ) + if accepted_archive: + receipt = { + "schema": 2, + "repository": "tox-dev/fixture", + "tag": "v0.1.11", + "commit": "0" * 40, + "sourceRunId": 41, + "pluginId": 29973, + "version": "0.1.11", + "channel": "beta", + "signedArchive": accepted_archive.name, + "sha256": sha256(accepted_archive.read_bytes()).hexdigest(), + "signedArtifactId": 51, + "notesArtifactId": 52, + "notesSha256": sha256(b"Release notes\n").hexdigest(), + } + receipt.update(receipt_changes or {}) + for artifact_id, filename, payload in ( + (61, "other.json" if missing_receipt else "marketplace-acceptance.json", json.dumps(receipt).encode()), + ( + 51, + accepted_archive.name, + prior_signed if prior_signed is not None else accepted_archive.read_bytes(), + ), + (52, "release-notes.md", prior_notes if prior_notes is not None else b"Release notes\n"), + ): + archive = BytesIO() + with ZipFile(archive, "w", ZIP_DEFLATED) as content: + content.writestr(filename, payload) + server.expect_request( + f"/api/v3/repos/tox-dev/fixture/actions/artifacts/{artifact_id}/zip" + ).respond_with_data(archive.getvalue(), content_type="application/zip") + notes_file = repository / "release-notes.md" + notes_file.write_text("Release notes\n") + return action( + repository, + runner_temp or repository, + scripts, + "publish-jetbrains-marketplace", + version="", + TAG="v0.1.11", + COMMIT="0" * 40, + SIGNED_ARTIFACT_ID="51", + NOTES_ARTIFACT_ID="52", + NOTES_FILE=str(notes_file), + GITHUB_REF="refs/tags/v0.1.11", + GITHUB_WORKFLOW_REF="tox-dev/fixture/.github/workflows/release.yaml@refs/tags/v0.1.11", + GITHUB_RUN_ID="42", + GITHUB_RUN_ATTEMPT="1", + **environment, + **settings, + ) + + @pytest.fixture def signed_archive(tmp_path: Path) -> Path: archive = tmp_path / "Fish Shell 2-0.1.11-signed.zip" @@ -143,7 +507,11 @@ def signed_archive(tmp_path: Path) -> Path: def marketplace_scripts(action_scripts: Path, tmp_path: Path, httpserver: HTTPServer) -> Path: scripts = tmp_path / "scripts" shutil.copytree(action_scripts, scripts) - upload = scripts / "publish-jetbrains-marketplace-1.sh" + steps = yaml.safe_load( + (Path(__file__).resolve().parents[2] / "publish-jetbrains-marketplace/action.yml").read_text() + )["runs"]["steps"] + upload_index = next(index for index, step in enumerate(steps) if step.get("name") == "Upload signed ZIP") + upload = scripts / f"publish-jetbrains-marketplace-{upload_index}.sh" upload.write_text( upload.read_text().replace( "https://plugins.jetbrains.com/api/updates/upload", httpserver.url_for("/api/updates/upload") From 6ef05d6471693dfdf18c544b49d6df2610bf4df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sat, 24 Oct 2026 02:24:42 -0700 Subject: [PATCH 2/4] feat(release): lock project selections Use the released full-lock exporter for PR checks and weekly refreshes while keeping privileged installs on selected cooled groups. Check frozen hook revisions and their complete cooled dependency closure before PR merge and tagged release execution. Preserve secondary Nab build matrices. --- .github/workflows/check.yaml | 42 +- .../workflows/prepare-towncrier-release.yaml | 16 +- .github/workflows/release.yaml | 9 +- .github/workflows/upgrade-release-tools.yaml | 4 +- .../workflows/validate-python-release.yaml | 8 +- .pre-commit-config.yaml | 18 +- README.md | 145 +- changelog.d/+full-project-lock.feature.md | 2 + changelog.d/+hook-closure.bugfix.md | 3 + changelog.d/+marketplace-retry.breaking.md | 3 +- check-hook-update-cooldown/action.yml | 125 +- prepare-towncrier/action.yml | 6 + publish-jetbrains-marketplace/action.yml | 77 +- pylock.toml | 5394 ++++++++++++----- pyproject.toml | 23 +- recheck-hook-update-cooldown/action.yml | 10 +- refresh-release-lock/action.yml | 93 +- .../test_cooldown.py | 301 +- .../test_frozen_hooks.py | 72 + .../test_hook_config.py | 100 +- .../test_workflow.py | 70 +- tests/conftest.py | 5 +- tests/export_build_constraints/test_export.py | 43 +- tests/install_locked_tools/test_install.py | 44 +- tests/install_locked_tools/test_metadata.py | 53 + .../test_marketplace_upload.py | 331 +- .../test_recheck.py | 19 +- tests/refresh_release_lock/test_cutoffs.py | 165 +- tests/refresh_release_lock/test_provider.py | 30 + tests/release_workflow/test_graph.py | 2 +- tests/upgrade_release_tools/test_upgrade.py | 8 +- tests/validate_news/test_lock_groups.py | 301 +- tests/validate_news/test_news.py | 2 + tests/verify_pypi_files/test_verify.py | 2 +- tools/build_constraints.py | 6 +- tools/check_frozen_hooks.py | 43 + tools/check_hook_config.py | 38 +- tools/check_lock_groups.py | 52 +- tools/check_recent_cutoffs.py | 15 +- tools/expire_cutoffs.py | 11 +- tools/lock_groups.py | 91 +- tools/nab_cutoffs.py | 71 +- tools/verify_cutoff_patch.py | 36 +- validate-news/action.yml | 2 +- validate-release-patch/action.yml | 7 + verify-frozen-hooks/action.yml | 18 + 46 files changed, 6011 insertions(+), 1905 deletions(-) create mode 100644 changelog.d/+full-project-lock.feature.md create mode 100644 changelog.d/+hook-closure.bugfix.md create mode 100644 tests/check_hook_update_cooldown/test_frozen_hooks.py create mode 100644 tests/refresh_release_lock/test_provider.py create mode 100644 tools/check_frozen_hooks.py create mode 100644 verify-frozen-hooks/action.yml diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index b97bd34..00180a0 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -40,7 +40,13 @@ jobs: - name: Check repository formatting env: UV_EXCLUDE_NEWER: 7 days - run: prek run --all-files + UV_NO_CONFIG: "1" + UV_NO_SOURCES: "1" + run: | + unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL + unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE + unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR + prek run --all-files shell: runs-on: ubuntu-26.04 permissions: @@ -68,7 +74,14 @@ jobs: groups: release-shell venv: .release-tools - name: Check inline shell + env: + UV_EXCLUDE_NEWER: 7 days + UV_NO_CONFIG: "1" + UV_NO_SOURCES: "1" run: | + unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL + unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE + unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR set -euo pipefail script_dir=./inline-shell-check mkdir "$script_dir" @@ -139,33 +152,10 @@ jobs: - uses: $/check-hook-update-cooldown with: release-app-client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - release-tools-preview: - if: github.event_name == 'pull_request' - runs-on: ubuntu-26.04 - permissions: - contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 - with: - version: latest-known - python-version: "3.15" - enable-cache: true - cache-suffix: ${{ github.job }} - cache-dependency-glob: | - pylock.toml - pyproject.toml - - uses: $/refresh-release-lock - with: - lock-groups: >- - build lock-tools lock-reader release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check - mode: check gate: name: ${{ github.event_name == 'pull_request' && 'Required checks pass' || 'Required checks pass (non-PR)' }} if: ${{ !cancelled() }} - needs: [check, shell, test, type, hook-cooldown, release-tools-preview] + needs: [check, shell, test, type, hook-cooldown] runs-on: ubuntu-26.04 steps: - env: @@ -177,6 +167,6 @@ jobs: to_entries[]; .value.result == "success" or ($ENV.EVENT_NAME != "pull_request" and .value.result == "skipped" and - (.key == "hook-cooldown" or .key == "release-tools-preview")) + .key == "hook-cooldown") ) ' <<<"$NEEDS_JSON" diff --git a/.github/workflows/prepare-towncrier-release.yaml b/.github/workflows/prepare-towncrier-release.yaml index b8677c7..0b9b7a4 100644 --- a/.github/workflows/prepare-towncrier-release.yaml +++ b/.github/workflows/prepare-towncrier-release.yaml @@ -61,11 +61,18 @@ jobs: groups: release-prepare${{ inputs.local-tool-source && ' project-runtime' || '' }} venv: .venv local-tool-source: ${{ inputs.local-tool-source }} + - uses: $/verify-frozen-hooks - name: Install prek hooks id: hooks env: UV_EXCLUDE_NEWER: 7 days - run: prek install-hooks + UV_NO_CONFIG: "1" + UV_NO_SOURCES: "1" + run: | + unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL + unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE + unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR + prek install-hooks background: true - name: Build local release tool wheel if: inputs.local-tool-source @@ -188,13 +195,18 @@ jobs: version: ${{ needs.assemble.outputs.version }} patch: ${{ runner.temp }}/release-patch/release.patch checks: "false" + - id: repository + name: Select the current repository + env: + REPOSITORY: ${{ github.repository }} + run: echo "name=${REPOSITORY#*/}" >>"$GITHUB_OUTPUT" - id: token uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 with: client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} - repositories: ${{ github.event.repository.name }} + repositories: ${{ steps.repository.outputs.name }} permission-contents: write - uses: $/commit-and-tag with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 39f9895..a01019b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,10 +25,17 @@ jobs: with: groups: release-check venv: .venv + - uses: $/verify-frozen-hooks - name: Check tagged source env: UV_EXCLUDE_NEWER: 7 days - run: .venv/bin/prek run --all-files + UV_NO_CONFIG: "1" + UV_NO_SOURCES: "1" + run: | + unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL + unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE + unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR + .venv/bin/prek run --all-files source: runs-on: ubuntu-26.04 permissions: diff --git a/.github/workflows/upgrade-release-tools.yaml b/.github/workflows/upgrade-release-tools.yaml index ea03be3..e91eafe 100644 --- a/.github/workflows/upgrade-release-tools.yaml +++ b/.github/workflows/upgrade-release-tools.yaml @@ -3,8 +3,8 @@ on: workflow_call: inputs: lock-groups: - description: Release dependency groups selected by the universal lock - required: true + description: Legacy Nab dependency groups; ignored for full project locks + default: "" type: string additional-lock: description: Optional secondary pylock basename for a separate build matrix diff --git a/.github/workflows/validate-python-release.yaml b/.github/workflows/validate-python-release.yaml index 65905c1..236103e 100644 --- a/.github/workflows/validate-python-release.yaml +++ b/.github/workflows/validate-python-release.yaml @@ -35,10 +35,16 @@ jobs: with: groups: release-check venv: ${{ runner.temp }}/release-tools + - uses: $/verify-frozen-hooks - name: Run tagged source checks env: UV_EXCLUDE_NEWER: 7 days - run: >- + UV_NO_CONFIG: "1" + UV_NO_SOURCES: "1" + run: | + unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL + unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE + unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR "$RUNNER_TEMP/release-tools/bin/prek" run --all-files constraints: runs-on: ubuntu-26.04 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0810075..73da088 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,14 +5,22 @@ repos: rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 hooks: - id: end-of-file-fixer + additional_dependencies: + - "ruamel-yaml==0.19.1" # release-creator managed - id: trailing-whitespace + additional_dependencies: + - "ruamel-yaml==0.19.1" # release-creator managed - repo: https://github.com/astral-sh/ruff-pre-commit rev: 38bfb07c0c64c304f47eb52090f8288150c77730 # v0.17.1 hooks: - id: ruff-check args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] + additional_dependencies: + - "ruff==0.17.1" # release-creator managed - id: ruff-format types_or: [python, pyi] + additional_dependencies: + - "ruff==0.17.1" # release-creator managed - repo: local hooks: - id: mdformat @@ -20,7 +28,13 @@ repos: entry: mdformat --wrap 120 language: python types: [markdown] - additional_dependencies: [mdformat-gfm==1.0.0] + additional_dependencies: + - mdformat-gfm==1.0.0 + - "markdown-it-py==4.2.0" # release-creator managed + - "mdformat==1.0.0" # release-creator managed + - "mdit-py-plugins==0.6.1" # release-creator managed + - "mdurl==0.1.2" # release-creator managed + - "wcwidth==0.9.2" # release-creator managed - repo: https://github.com/scop/pre-commit-shfmt rev: 479be5958357ba5ab65ce47172117348516860e9 # v3.14.1-1 hooks: @@ -38,3 +52,5 @@ repos: rev: c92afdf2c6ca336aaa8f39e2bd8d48f62dbd8e1f # v1.31.1 hooks: - id: zizmor + additional_dependencies: + - "zizmor==1.31.1" # release-creator managed diff --git a/README.md b/README.md index a49e2b8..bf51935 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,16 @@ keeps its dispatch form, publisher, GitHub environment, and required check names `install-locked-tools` creates a virtual environment and installs selected groups from the caller's `pylock.toml` by default. `lock-source: shared` selects this Action repository's `pylock.toml` for shared jobs such as hook maintenance -that do not need the caller's release tools. Nab's universal lock records the selected release groups for Linux, macOS, -and Windows. It resolves dependencies under the seven-day cutoff in `[tool.nab]`; test, type, and documentation -dependencies resolve fresh. The Action rejects missing groups and leaves tracked files unchanged. Source builds use -constraints generated from the lock's `build` group under the runner's temporary directory. +that do not need the caller's release tools. The caller's full project lock records runtime dependencies, every extra, +and every dependency group across supported Python versions and platforms. CI resolves test, type, and documentation +dependencies fresh; release jobs install only the selected release groups. The Action rejects missing groups and leaves +tracked files unchanged. Source builds use constraints generated from the lock's `build` group under the runner's +temporary directory. + +Select the groups each job needs. The shared workflows use `release-news release-shell`, +`release-hook-check release-notes lock-reader`, and `release-prepare project-runtime` or +`release-validate project-runtime` for release-creator's own build. A full lock records each group and extra, but a new +combination still needs an install check before a caller uses it. - `venv`: directory for the virtual environment when `groups` is set. - `groups`: whitespace-separated release dependency groups. Leave empty to export build constraints without installing @@ -31,8 +37,8 @@ constraints generated from the lock's `build` group under the runner's temporary The `build-constraints` output names the hashed constraints file. The Action adds installed command launchers to `GITHUB_PATH` for later steps while leaving the runner's `python`, `pip`, and `uv` commands on their original paths. Run -`actions/checkout` and `astral-sh/setup-uv` with Python 3.15 before it. It bootstraps Nab from the shared, hash-checked -lock. +`actions/checkout` and `astral-sh/setup-uv` with Python 3.15 before it. It bootstraps its lock reader from the shared, +hash-checked lock. For a zipapp that bundles wheels from a separate runtime lock, check that lock in the same job before building it: @@ -87,13 +93,14 @@ The Action expects a pull request base ref and a Markdown or reStructuredText ch ## Refresh release tools and hooks Call `upgrade-release-tools.yaml` from a scheduled or manual workflow. Its read-only jobs refresh the universal -`pylock.toml` and the frozen pre-commit hooks in parallel. Nab reads the seven-day package cutoff and any reviewed -package exceptions from `[tool.nab]`. The hook updater applies the same cooldown to hook revisions and their -`additional_dependencies`. Each job uploads a patch. The lock patch may change `pylock.toml`, an explicitly configured -secondary lock, and `pyproject.toml` for expired Nab cutoffs. The hook patch may change only `.pre-commit-config.yaml`. -The writer checks each patch before applying it and verifies cutoff removals before minting the App token. It then opens -or updates one dependency PR from `automation/update-release-tools` to `main`. A lock or hook update runs the normal PR -checks before merge. +`pylock.toml` and the frozen pre-commit hooks in parallel. `release-creator locks` exports runtime dependencies, all +extras, and all dependency groups with a seven-day package cutoff. Reviewed exceptions live in +`[tool.release-creator.locks.exclude-newer-package]`. The hook updater applies the same cooldown to hook revisions and +their `additional_dependencies`. Each job uploads a patch. The lock patch may change `pylock.toml`, an explicitly +configured secondary Nab lock, and `pyproject.toml` for expired reviewed cutoffs. The hook patch may change only +`.pre-commit-config.yaml`. The writer checks each patch before applying it and verifies cutoff removals before minting +the App token. It then opens or updates one dependency PR from `automation/update-release-tools` to `main`. A lock or +hook update runs the normal PR checks before merge. The writer uses a repository-only release App with Contents and Pull requests write access. Install the App on this repository, set `RELEASE_APP_CLIENT_ID` as a repository variable, and store `RELEASE_APP_PRIVATE_KEY` in the @@ -116,20 +123,12 @@ jobs: contents: read secrets: RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - with: - lock-groups: >- - build release-notes release-news release-prepare - release-validate release-check release-attestations release-shell - release-hooks release-hook-check ``` -The required `lock-groups` input lists the caller's release dependency groups, separated by spaces or newlines. Include -`build` and the release groups used by the caller. `lock-tools` and `lock-reader` come from this Action's lock; include -them only when the caller also uses them. Exclude test, type, and documentation groups. The project configures targets -in `[tool.nab]`. Tests install test, type, and documentation groups afresh so they detect upstream dependency changes. -The Action tests fetch immutable, hash-checked release-tool artifacts from the shared lock; their separate package -fixture resolves test-only tools afresh. The updater also refreshes a caller's secondary build-matrix lock when -`additional-lock` and its matrix inputs are set. +`lock-groups` remains an input for callers with an older Nab primary lock; the full project exporter ignores it and +records every dependency group and extra. Tests install test, type, and documentation tools afresh to detect upstream +dependency changes. The updater also refreshes a caller's secondary Nab build-matrix lock when `additional-lock` and its +matrix inputs are set. The optional `commit-message`, `pull-request-title`, and `pull-request-body` inputs retain existing PR wording. The workflow does not update `uv.lock` or a checked-in constraints file. Build constraints are generated from the lock's @@ -143,49 +142,30 @@ both locks in the same dependency PR. The writer accepts only those locks, `pypr Pass the same `additional-lock` and matrix inputs to `validate-news` in the PR workflow. It checks changes to either lock against the project, including a PR that changes only the secondary lock. -`refresh-release-lock` is the read-only half of the updater. In `refresh` mode, it upgrades the selected release groups -under Nab's configured cutoff and writes `$RUNNER_TEMP/lock.patch` if either lock or a reviewed expired Nab cutoff -changes. In `check` mode, it regenerates without upgrading and fails if the checked-in files differ. The resulting PR's -normal formatting job runs `prek`; the writer checks patch paths and modes. Its `changed` output is `true` only when the -refresh produces a patch. Neither mode pushes a branch or requests a write token. Check it on a PR in a job with -Contents read permission: - -```yaml -release-tools-preview: - if: github.event_name == 'pull_request' - runs-on: ubuntu-26.04 - permissions: - contents: read - steps: - - uses: actions/checkout@CHECKOUT_SHA - with: - persist-credentials: false - - uses: astral-sh/setup-uv@SETUP_UV_SHA - with: - version: latest-known - python-version: '3.15' - - uses: tox-dev/github-workflows/refresh-release-lock@SHA - with: - lock-groups: >- - build lock-tools lock-reader release-notes release-news release-prepare - release-validate release-check release-attestations release-shell - release-hooks release-hook-check - mode: check -``` +`refresh-release-lock` is the read-only half of the updater. In `refresh` mode, it upgrades the full primary lock and +writes `$RUNNER_TEMP/lock.patch` if a lock or reviewed cutoff changes. In `check` mode, it regenerates at the primary +lock's recorded creation time and fails if the checked-in files differ. `validate-news` calls this mode when a PR +changes release inputs, including a workspace member's `pyproject.toml` or a secondary lock. The resulting PR's normal +formatting job runs `prek`; the writer checks patch paths and modes. Its `changed` output is `true` only when the +refresh produces a patch. Neither mode pushes a branch or requests a write token. The weekly updater freezes eligible hook revisions with `prek update --freeze --cooldown-days 7`. It then uses `release-creator hooks` to pin Python hook dependencies under the same cutoff and writes a patch when `.pre-commit-config.yaml` changes. The generation job has read-only repository permissions; the separate publish job checks the patch paths and file modes before obtaining an App token. -`check-hook-update-cooldown` checks proposed hook revisions on bot PRs. Put it in the caller's `pull_request` workflow -with `actions: read`, `contents: read`, and `pull-requests: read`, then include it in the required aggregate. It reads -its PR number and head SHA from the event, compares them with the live PR, and requires each proposed revision to have -appeared in a GitHub PR workflow run at least `minimum-days` earlier. The default is seven days. A force push does not -reset an unchanged revision's age or give a new revision the old one's age. Human PRs pass without installing hook -tools. Other bot PRs may change hook revisions only. On `automation/update-release-tools`, the Action verifies the bot -against `RELEASE_APP_CLIENT_ID` and permits exact, cooled PyPI dependency pins generated by the release App. It keeps -the same seven-day revision observation rule, so current revisions may remain pending after the lock refresh. +`check-hook-update-cooldown` checks every PR that changes `.pre-commit-config.yaml`. Put it in the caller's +`pull_request` workflow with `actions: read`, `contents: read`, and `pull-requests: read`, then include it in the +required aggregate. It reads its PR number and head SHA from the event, compares them with the live PR, and reads the +proposed configuration from that exact head, including a fork head. It checks the complete Python hook dependency +closure at the configured `minimum-days` cutoff and requires each changed revision to have appeared in a GitHub PR +workflow run at least that many days earlier. The default is seven days. A force push does not reset an unchanged +revision's age or give a new revision the old one's age. Human PRs may add hooks or change their options under the same +revision and dependency checks, but only the release App may change managed dependency pins. Other bot PRs may change +revisions only. On `automation/update-release-tools`, the Action verifies the bot against `RELEASE_APP_CLIENT_ID` and +permits exact, cooled PyPI dependency pins generated by the release App. Every external hook revision must be a full +lowercase commit SHA. A revision with new Python dependencies stays blocked until the App updates its managed pins. +Tagged source jobs check frozen revisions and the complete Python dependency closure before running `prek`. ```yaml hook-cooldown: @@ -204,9 +184,9 @@ hook-cooldown: Run the PR workflow on `opened`, `reopened`, and `synchronize`; the cooldown check must succeed on PRs and may skip on push. The Action has no output or write side effect. Schedule `recheck-hook-update-cooldown` daily with `actions: write`, `contents: read`, and `pull-requests: read`. It reruns eligible failed cooldown jobs at the current -head. GitHub accepts reruns for 30 days. A failed rerun is retried on the next schedule while eligible. The -[caller here](.github/workflows/recheck-hook-cooldown.yaml) shows the trigger and permissions. The Action reruns the -`hook-cooldown` job in `check.yaml`. +head, including human and fork PRs. GitHub accepts reruns for 30 days. A failed rerun is retried on the next schedule +while eligible. The [caller here](.github/workflows/recheck-hook-cooldown.yaml) shows the trigger and permissions. The +Action reruns the `hook-cooldown` job in `check.yaml`. If the job has a different name or workflow filename, pass both to the scheduled caller: @@ -676,6 +656,7 @@ marketplace-publish: runs-on: ubuntu-26.04 permissions: actions: read + deployments: read environment: release steps: - uses: actions/download-artifact@DOWNLOAD_SHA @@ -703,18 +684,20 @@ marketplace-publish: ``` `channel` is empty for Stable; use a Marketplace channel name for a prerelease. Keep the publishing step named -`Publish signed ZIP`, since the retry check reads that step from earlier workflow runs. Give the job Actions read access -and the workflow a concurrency group keyed by tag. The Action verifies that the remote tag still targets the release -commit, then searches earlier runs for the tag. A prior upload from another commit stops for manual reconciliation. For -the same commit, an acceptance receipt must match the original artifact IDs, signed ZIP digest, and release notes bytes -before the Action skips upload. If an earlier attempt reached the upload step without a receipt, it also stops; -Marketplace may have accepted the upload before the run failed. +`Publish signed ZIP`. The Action recognizes that name and two older upload step names in prior runs and deployment jobs. +Keep those names in the Action when adding another. Give the job Actions and Deployments read access and the workflow a +concurrency group keyed by tag. Use the `release` environment on the upload job. The Action verifies the remote tag, +then searches earlier tag runs and release deployments. For the same commit, an acceptance receipt must match the +original artifact IDs, signed ZIP digest, and release notes bytes before the Action skips upload. Any earlier release +deployment without a verified receipt stops for manual reconciliation, even if that run did not reach the Marketplace +step. The same applies when the linked job is no longer available. A changed publishing step name fails before upload +until the Action recognizes it. **Output:** A `marketplace-acceptance--` Actions artifact after a new upload. It records the tag, commit, plugin, channel, signed ZIP artifact and SHA256, and notes artifact and SHA256. **Remote effect:** One multipart upload -to JetBrains Marketplace when no earlier acceptance exists, plus a GitHub Actions receipt artifact. The Action treats -any successful HTTP response as acceptance; it does not depend on undocumented response fields. Run the keyless -release-creator `marketplace --mode stage` check before this job and `--mode verify` after the version is public. +to JetBrains Marketplace when no earlier acceptance exists, plus a GitHub Actions receipt artifact. The Action treats an +HTTP 2xx response as acceptance; it does not depend on undocumented response fields. Run the keyless release-creator +`marketplace --mode stage` check before this job and `--mode verify` after the version is public. If the Action stops without a receipt, inspect the exact plugin, version, and channel in the Marketplace dashboard. If the update is pending review, wait until it is public and verify its member bytes against the original signed ZIP. Then @@ -722,6 +705,20 @@ rerun the tag workflow: the keyless stage check must skip Marketplace upload bef Marketplace rejected the update, resolve the rejection and prepare a new release tag. A 404 alone does not authorize another upload of the same tag. +If an earlier `release` deployment blocks a retry, inspect its linked run and job. Only when its logs prove the +Marketplace upload step never started, mark that deployment inactive and delete it with the +[GitHub deployment API](https://docs.github.com/en/rest/deployments/deployments#delete-a-deployment), then rerun the tag +workflow: + +```shell +gh api -X POST repos/OWNER/REPO/deployments/ID/statuses -f state=inactive +gh api -X DELETE repos/OWNER/REPO/deployments/ID +``` + +If the run or its logs have expired, do not delete the deployment based on absence from the public Marketplace download +API: JetBrains may still be reviewing the upload. Ask JetBrains to confirm its status or cut a new release tag and +version. + ## Test a caller The [Actions test suite](tests) executes each inline `run:` block in temporary Git repositories. Test a caller's diff --git a/changelog.d/+full-project-lock.feature.md b/changelog.d/+full-project-lock.feature.md new file mode 100644 index 0000000..5409f15 --- /dev/null +++ b/changelog.d/+full-project-lock.feature.md @@ -0,0 +1,2 @@ +Refresh and check a selectable project lock containing runtime dependencies, every extra, and every dependency group. +Release jobs still install only their selected, cooled groups. diff --git a/changelog.d/+hook-closure.bugfix.md b/changelog.d/+hook-closure.bugfix.md new file mode 100644 index 0000000..a41057a --- /dev/null +++ b/changelog.d/+hook-closure.bugfix.md @@ -0,0 +1,3 @@ +Check the cooldown, immutable hook revisions, and complete Python hook dependency pins on every pull request that +changes the hook configuration, including human and fork pull requests. Check the hook pins again before running hooks +from a release tag. Run hook installs from PyPI with the seven-day cutoff, ignoring repository uv configuration. diff --git a/changelog.d/+marketplace-retry.breaking.md b/changelog.d/+marketplace-retry.breaking.md index 2ae92b1..7787b6e 100644 --- a/changelog.d/+marketplace-retry.breaking.md +++ b/changelog.d/+marketplace-retry.breaking.md @@ -1,3 +1,4 @@ Require a release tag, source commit, signed ZIP artifact, and release notes artifact when publishing a JetBrains plugin. The Action now records an acceptance receipt and blocks an ambiguous retry until a maintainer reconciles the -Marketplace update. +Marketplace update. Publishing now requires Deployments read access and a `release` environment so retries can detect +prior attempts after Actions runs or receipt artifacts expire. diff --git a/check-hook-update-cooldown/action.yml b/check-hook-update-cooldown/action.yml index 798da8b..f11d37d 100644 --- a/check-hook-update-cooldown/action.yml +++ b/check-hook-update-cooldown/action.yml @@ -1,5 +1,5 @@ name: Check hook update cooldown -description: Require seven days before a pre-commit.ci update pull request can merge +description: Require seven days before a hook update pull request can merge inputs: minimum-days: description: Minimum age of a proposed hook revision @@ -35,17 +35,19 @@ runs: .head.sha == $head and (.user.login | type == "string") ' "$RUNNER_TEMP/hook-update-pr.json" >/dev/null - if [[ $(jq -r .user.type "$RUNNER_TEMP/hook-update-pr.json") == Bot ]]; then - jq -e --arg repository "$REPOSITORY" '.head.repo.full_name == $repository' \ - "$RUNNER_TEMP/hook-update-pr.json" >/dev/null || { - echo '::error::bot hook updates must come from this repository' >&2 - exit 1 - } - gh api --paginate "repos/$REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename' \ - >"$RUNNER_TEMP/hook-update-files" - fi - if [[ -f "$RUNNER_TEMP/hook-update-files" ]] && - grep -Fxq .pre-commit-config.yaml "$RUNNER_TEMP/hook-update-files"; then + gh api --paginate "repos/$REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename' \ + >"$RUNNER_TEMP/hook-update-files" + if grep -Fxq .pre-commit-config.yaml "$RUNNER_TEMP/hook-update-files"; then + if [[ $(jq -r .user.type "$RUNNER_TEMP/hook-update-pr.json") == Bot ]]; then + jq -e --arg repository "$REPOSITORY" '.head.repo.full_name == $repository' \ + "$RUNNER_TEMP/hook-update-pr.json" >/dev/null || { + echo '::error::bot hook updates must come from this repository' >&2 + exit 1 + } + fi + if [[ $(jq -r .user.type "$RUNNER_TEMP/hook-update-pr.json") == User ]]; then + echo 'human=true' >>"$GITHUB_OUTPUT" + fi if [[ $(jq -r .head.ref "$RUNNER_TEMP/hook-update-pr.json") == automation/update-release-tools ]]; then author=$(jq -r .user.login "$RUNNER_TEMP/hook-update-pr.json") [[ "$author" == *'[bot]' && -n "$RELEASE_APP_CLIENT_ID" ]] || { @@ -64,7 +66,7 @@ runs: fi gh api --paginate -X GET "repos/$REPOSITORY/actions/runs" -f event=pull_request \ -f head_sha="$HEAD_SHA" -f per_page=100 >"$RUNNER_TEMP/hook-update-runs.jsonl" - if jq -se --arg repository "$REPOSITORY" \ + if jq -se --arg repository "$(jq -r .head.repo.full_name "$RUNNER_TEMP/hook-update-pr.json")" \ --arg branch "$(jq -r .head.ref "$RUNNER_TEMP/hook-update-pr.json")" \ --arg head "$HEAD_SHA" --argjson number "$PR_NUMBER" --argjson days "$MINIMUM_DAYS" ' [.[].workflow_runs[] | select( @@ -76,22 +78,22 @@ runs: ' "$RUNNER_TEMP/hook-update-runs.jsonl" >/dev/null; then echo 'cooled=true' >>"$GITHUB_OUTPUT" fi - echo 'bot=true' >>"$GITHUB_OUTPUT" + echo 'changed=true' >>"$GITHUB_OUTPUT" fi - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 - if: steps.identity.outputs.bot == 'true' + if: steps.identity.outputs.changed == 'true' with: version: latest-known python-version: "3.15" enable-cache: false - uses: $/install-locked-tools - if: steps.identity.outputs.bot == 'true' + if: steps.identity.outputs.changed == 'true' with: - groups: release-hook-check lock-reader + groups: release-hook-check release-notes lock-reader lock-source: shared venv: ${{ runner.temp }}/hook-tools - name: Read the base and proposed hook configurations - if: steps.identity.outputs.bot == 'true' + if: steps.identity.outputs.changed == 'true' shell: bash env: GH_TOKEN: ${{ github.token }} @@ -100,61 +102,74 @@ runs: REPOSITORY=$GITHUB_REPOSITORY HEAD_SHA=$(jq -r .pull_request.head.sha "$GITHUB_EVENT_PATH") base_sha=$(jq -r .base.sha "$RUNNER_TEMP/hook-update-pr.json") + head_repository=$(jq -r .head.repo.full_name "$RUNNER_TEMP/hook-update-pr.json") gh api "repos/$REPOSITORY/contents/.pre-commit-config.yaml?ref=$base_sha" --jq .content | base64 --decode >"$RUNNER_TEMP/base-hooks.yaml" - gh api "repos/$REPOSITORY/contents/.pre-commit-config.yaml?ref=$HEAD_SHA" --jq .content | + gh api "repos/$head_repository/contents/.pre-commit-config.yaml?ref=$HEAD_SHA" --jq .content | base64 --decode >"$RUNNER_TEMP/proposed-hooks.yaml" - cp "$RUNNER_TEMP/base-hooks.yaml" "$RUNNER_TEMP/eligible-hooks.yaml" git init -q "$RUNNER_TEMP/hook-cooldown" + - name: Reject movable hook revisions + if: steps.identity.outputs.changed == 'true' + shell: bash + run: >- + uv run --no-project --python "$RUNNER_TEMP/hook-tools" python "$GITHUB_ACTION_PATH/../tools/check_frozen_hooks.py" "$RUNNER_TEMP/proposed-hooks.yaml" - name: Reject non-revision hook changes - if: steps.identity.outputs.bot == 'true' + if: steps.identity.outputs.changed == 'true' shell: bash env: MANAGED: ${{ steps.identity.outputs.managed }} + HUMAN: ${{ steps.identity.outputs.human }} MINIMUM_DAYS: ${{ inputs.minimum-days }} run: | set -euo pipefail uv run --no-project --python "$RUNNER_TEMP/hook-tools" python \ "$GITHUB_ACTION_PATH/../tools/check_hook_config.py" \ "$RUNNER_TEMP/base-hooks.yaml" "$RUNNER_TEMP/proposed-hooks.yaml" \ - --managed "$MANAGED" --minimum-days "$MINIMUM_DAYS" - - name: Select eligible hook revisions - if: steps.identity.outputs.bot == 'true' + --managed "$MANAGED" --human "$HUMAN" --minimum-days "$MINIMUM_DAYS" + - name: Verify managed hook dependency closure + if: steps.identity.outputs.changed == 'true' shell: bash env: MINIMUM_DAYS: ${{ inputs.minimum-days }} run: | - uv run --no-project --python "$RUNNER_TEMP/hook-tools" prek update -C "$RUNNER_TEMP/hook-cooldown" \ - --config "$RUNNER_TEMP/eligible-hooks.yaml" \ - --cooldown-days "$MINIMUM_DAYS" --no-progress + set -euo pipefail + mkdir -p "$RUNNER_TEMP/hook-closure" + cp "$RUNNER_TEMP/proposed-hooks.yaml" "$RUNNER_TEMP/hook-closure/.pre-commit-config.yaml" + cd "$RUNNER_TEMP/hook-closure" + uv run --no-project --python "$RUNNER_TEMP/hook-tools" release-creator hooks \ + --check --minimum-days "$MINIMUM_DAYS" - name: Compare proposed hook revisions - if: steps.identity.outputs.bot == 'true' + if: steps.identity.outputs.changed == 'true' shell: bash + env: + HUMAN: ${{ steps.identity.outputs.human }} run: | set -euo pipefail - for config in base proposed eligible; do + for config in base proposed; do uv run --no-project --python "$RUNNER_TEMP/hook-tools" yq -c \ '[.repos[] | select(.repo != "local" and .repo != "meta") | {repo, rev}]' \ "$RUNNER_TEMP/$config-hooks.yaml" >"$RUNNER_TEMP/$config-hooks.json" done jq -e -n \ --slurpfile base "$RUNNER_TEMP/base-hooks.json" \ - --slurpfile proposed "$RUNNER_TEMP/proposed-hooks.json" \ - --slurpfile eligible "$RUNNER_TEMP/eligible-hooks.json" ' - $base[0] as $old | - $eligible[0] as $allowed | - $proposed[0] as $new | - if ($old | map(.repo)) != ($new | map(.repo)) or - ($old | map(.repo)) != ($allowed | map(.repo)) + --slurpfile proposed "$RUNNER_TEMP/proposed-hooks.json" --arg human "$HUMAN" ' + def identities($entries): + [$entries | to_entries[] | . as $entry | + {repo: .value.repo, rev: .value.rev, + occurrence: ([$entries[:$entry.key][] | select(.repo == $entry.value.repo)] | length)}]; + identities($base[0]) as $old | + identities($proposed[0]) as $new | + if $human != "true" and + ($old | map(.repo)) != ($new | map(.repo)) then error("hook repository entries changed") - else [range(0; $new | length) as $index | - $new[$index] | - select(.rev != $old[$index].rev and .rev != $allowed[$index].rev) | - [.repo, .rev, ([$new[:$index][] | select(.repo == $new[$index].repo)] | length)]] end + else [$new[] | . as $entry | + select(.rev != ([$old[] | + select(.repo == $entry.repo and .occurrence == $entry.occurrence) | .rev][0] // null)) | + [.repo, .rev, .occurrence]] end ' >"$RUNNER_TEMP/older-hook-revisions.json" jq -r '.[] | @tsv' "$RUNNER_TEMP/older-hook-revisions.json" >"$RUNNER_TEMP/older-hook-revisions" - name: Find prior pull request heads - if: steps.identity.outputs.bot == 'true' && steps.identity.outputs.cooled != 'true' + if: steps.identity.outputs.changed == 'true' && steps.identity.outputs.cooled != 'true' shell: bash env: MINIMUM_DAYS: ${{ inputs.minimum-days }} @@ -167,7 +182,8 @@ runs: cutoff=$(jq -nr --argjson days "$MINIMUM_DAYS" 'now - $days * 86400 | todateiso8601') gh api --paginate -X GET "repos/$REPOSITORY/actions/runs" -f event=pull_request \ -f branch="$branch" -f "created=<$cutoff" -f per_page=100 >"$RUNNER_TEMP/hook-branch-runs.jsonl" - jq -sr --arg repository "$REPOSITORY" --arg branch "$branch" \ + jq -sr --arg repository "$(jq -r .head.repo.full_name "$RUNNER_TEMP/hook-update-pr.json")" \ + --arg branch "$branch" \ --argjson number "$PR_NUMBER" --argjson days "$MINIMUM_DAYS" ' [.[].workflow_runs[] | select( .event == "pull_request" and .head_repository.full_name == $repository and @@ -176,13 +192,13 @@ runs: ) | .head_sha] | unique[] ' "$RUNNER_TEMP/hook-branch-runs.jsonl" >"$RUNNER_TEMP/older-heads" - name: Read prior hook revisions - if: steps.identity.outputs.bot == 'true' && steps.identity.outputs.cooled != 'true' + if: steps.identity.outputs.changed == 'true' && steps.identity.outputs.cooled != 'true' shell: bash env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - REPOSITORY=$GITHUB_REPOSITORY + REPOSITORY=$(jq -r .head.repo.full_name "$RUNNER_TEMP/hook-update-pr.json") : >"$RUNNER_TEMP/observed-revisions.jsonl" while IFS= read -r head; do if gh api "repos/$REPOSITORY/contents/.pre-commit-config.yaml?ref=$head" --jq .content \ @@ -207,15 +223,19 @@ runs: ' "$RUNNER_TEMP/older-revisions.json" >>"$RUNNER_TEMP/observed-revisions.jsonl" done <"$RUNNER_TEMP/older-heads" - name: Verify the first GitHub observation of each revision - if: steps.identity.outputs.bot == 'true' && steps.identity.outputs.cooled != 'true' + if: steps.identity.outputs.changed == 'true' && steps.identity.outputs.cooled != 'true' shell: bash run: | jq -n --slurpfile base "$RUNNER_TEMP/base-hooks.json" \ --slurpfile proposed "$RUNNER_TEMP/proposed-hooks.json" ' - [$proposed[0] as $entries | $entries | to_entries[] | . as $entry | - select(.value.rev != $base[0][.key].rev) | - {repo: .value.repo, rev: .value.rev, - occurrence: ([$entries[:$entry.key][] | select(.repo == $entry.value.repo)] | length)}] + def identities($entries): + [$entries | to_entries[] | . as $entry | + {repo: .value.repo, rev: .value.rev, + occurrence: ([$entries[:$entry.key][] | select(.repo == $entry.value.repo)] | length)}]; + identities($base[0]) as $old | + [identities($proposed[0])[] | . as $entry | + select(.rev != ([$old[] | + select(.repo == $entry.repo and .occurrence == $entry.occurrence) | .rev][0] // null))] ' >"$RUNNER_TEMP/proposed-revisions.json" if ! jq -se --slurpfile proposed "$RUNNER_TEMP/proposed-revisions.json" ' . as $observed | @@ -227,7 +247,7 @@ runs: exit 1 fi - name: Verify older pinned revisions - if: steps.identity.outputs.bot == 'true' + if: steps.identity.outputs.changed == 'true' shell: bash env: MINIMUM_DAYS: ${{ inputs.minimum-days }} @@ -252,7 +272,10 @@ runs: fi accepted=false for tag in "${tags[@]}"; do - cp "$RUNNER_TEMP/base-hooks.yaml" "$RUNNER_TEMP/candidate-hooks.yaml" + cp "$RUNNER_TEMP/proposed-hooks.yaml" "$RUNNER_TEMP/candidate-hooks.yaml" + uv run --no-project --python "$RUNNER_TEMP/hook-tools" yq -y -i --arg repository "$repository" \ + "(.repos[] | select(.repo == \$repository) | .rev) = \"0000000000000000000000000000000000000000\"" \ + "$RUNNER_TEMP/candidate-hooks.yaml" uv run --no-project --python "$RUNNER_TEMP/hook-tools" prek update -C "$RUNNER_TEMP/hook-cooldown" \ --config "$RUNNER_TEMP/candidate-hooks.yaml" --repo "$repository" \ --repo-include-tag "$repository=$tag" --cooldown-days "$MINIMUM_DAYS" \ diff --git a/prepare-towncrier/action.yml b/prepare-towncrier/action.yml index 653ad4c..ad5beb9 100644 --- a/prepare-towncrier/action.yml +++ b/prepare-towncrier/action.yml @@ -99,7 +99,13 @@ runs: env: CHANGELOG: ${{ steps.config.outputs.changelog }} FORMAT: ${{ steps.config.outputs.format }} + UV_EXCLUDE_NEWER: 7 days + UV_NO_CONFIG: "1" + UV_NO_SOURCES: "1" run: | + unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL + unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE + unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR set -euo pipefail case "$FORMAT" in markdown) formatter=mdformat ;; diff --git a/publish-jetbrains-marketplace/action.yml b/publish-jetbrains-marketplace/action.yml index e027ba4..da545ea 100644 --- a/publish-jetbrains-marketplace/action.yml +++ b/publish-jetbrains-marketplace/action.yml @@ -29,7 +29,7 @@ inputs: description: Release notes downloaded from notes-artifact-id required: true github-token: - description: Token with Actions read permission + description: Token with Actions and Deployments read permissions required: true publish-token: description: JetBrains Marketplace upload token @@ -88,9 +88,8 @@ runs: COMMIT: ${{ inputs.commit }} run: | set -euo pipefail - workflow_ref=${GITHUB_WORKFLOW_REF%@*} - workflow_file=${workflow_ref##*/} - gh api --paginate "repos/$GITHUB_REPOSITORY/actions/workflows/$workflow_file/runs?per_page=100" \ + encoded_tag=$(jq -rn --arg tag "$TAG" '$tag | @uri') + gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs?branch=$encoded_tag&per_page=100" \ --jq '.workflow_runs[] | [.id, .head_branch, .head_sha] | @tsv' >"$RUNNER_TEMP/marketplace-runs.tsv" prior=0 while IFS=$'\t' read -r run branch sha; do @@ -103,7 +102,8 @@ runs: while IFS= read -r attempt; do if [[ "$run" == "$GITHUB_RUN_ID" && "$attempt" -ge "$GITHUB_RUN_ATTEMPT" ]]; then continue; fi if [[ "$sha" != "$COMMIT" ]]; then - echo "::error::Tag $TAG had an earlier Marketplace upload on commit $sha. Reconcile run $run attempt $attempt manually." >&2 + echo "::error::Tag $TAG had an earlier Marketplace upload on commit $sha." \ + "Reconcile run $run attempt $attempt manually." >&2 exit 1 fi gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs/$run/artifacts?per_page=100" \ @@ -112,7 +112,8 @@ runs: receipt_id=$(awk -F '\t' -v name="$receipt_name" '$1 == name && $3 == "false" { print $2 }' \ "$RUNNER_TEMP/marketplace-artifacts-$run.tsv") if [[ -z "$receipt_id" || "$receipt_id" == *$'\n'* || "$prior" == 1 ]]; then - echo "::error::Marketplace upload may already be accepted. Reconcile run $run attempt $attempt manually." >&2 + echo '::error::Marketplace upload may already be accepted.' \ + "Reconcile run $run attempt $attempt manually." >&2 exit 1 fi prior=1 @@ -124,6 +125,62 @@ runs: done <"$RUNNER_TEMP/marketplace-attempts.txt" done <"$RUNNER_TEMP/marketplace-runs.tsv" echo "accepted=$prior" >>"$GITHUB_OUTPUT" + - name: Check earlier release deployments + if: steps.earlier.outputs.accepted == '0' + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + TAG: ${{ inputs.tag }} + COMMIT: ${{ inputs.commit }} + run: | + set -euo pipefail + encoded_tag=$(jq -rn --arg tag "$TAG" '$tag | @uri') + gh api --paginate "repos/$GITHUB_REPOSITORY/deployments?ref=$encoded_tag&environment=release&per_page=100" \ + --jq '.[] | [.id, .sha] | @tsv' >"$RUNNER_TEMP/marketplace-deployments.tsv" + current=0 + while IFS=$'\t' read -r deployment sha; do + target=$(gh api "repos/$GITHUB_REPOSITORY/deployments/$deployment/statuses?per_page=1" \ + --jq '.[0].log_url // ""') + prefix="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/" + if [[ "$target" != "$prefix"* ]]; then + echo "::error::Cannot identify release deployment $deployment. Reconcile tag $TAG manually." >&2 + exit 1 + fi + path=${target#"$prefix"} + run=${path%%/*} + job=${path##*/} + if [[ "$path" != "$run/job/$job" || ! "$run" =~ ^[0-9]+$ || ! "$job" =~ ^[0-9]+$ ]]; then + echo "::error::Cannot identify release deployment $deployment. Reconcile tag $TAG manually." >&2 + exit 1 + fi + identity=$(gh api "repos/$GITHUB_REPOSITORY/actions/jobs/$job" \ + --jq '[.run_id, .run_attempt, any(.steps[]; (.name == "Publish signed ZIP" or + .name == "Upload signed ZIP" or .name == "📤 Publish to JetBrains Marketplace") and + .started_at != null)] | @tsv') || { + echo "::error::Cannot inspect release deployment $deployment. Reconcile tag $TAG manually." >&2 + exit 1 + } + read -r job_run attempt upload_started <<<"$identity" + if [[ "$sha" != "$COMMIT" ]]; then + echo "::error::Tag $TAG has a release deployment on another commit." \ + "Reconcile deployment $deployment manually." >&2 + exit 1 + fi + if [[ "$run" != "$job_run" ]]; then + echo "::error::Cannot identify release deployment $deployment. Reconcile tag $TAG manually." >&2 + exit 1 + fi + if [[ "$run" != "$GITHUB_RUN_ID" || "$attempt" != "$GITHUB_RUN_ATTEMPT" ]]; then + echo "::error::Tag $TAG has a prior release deployment $deployment" \ + "from run $run attempt $attempt. Reconcile it manually." >&2 + exit 1 + fi + if [[ "$upload_started" == true ]]; then current=1; fi + done <"$RUNNER_TEMP/marketplace-deployments.tsv" + if ((current == 0)); then + echo "::error::Marketplace upload needs a release environment deployment for tag $TAG." >&2 + exit 1 + fi - name: Verify earlier Marketplace acceptance if: steps.earlier.outputs.accepted == '1' shell: bash @@ -157,14 +214,16 @@ runs: (.notesArtifactId | type == "number")' "$receipt" >/dev/null signed_id=$(jq -r .signedArtifactId "$receipt") notes_id=$(jq -r .notesArtifactId "$receipt") - [[ "$(awk -F '\t' '$1 == "plugin-distribution" && $3 == "false" { print $2 }' "$ARTIFACTS")" == "$signed_id" ]] || exit 1 - [[ "$(awk -F '\t' '$1 == "release-notes" && $3 == "false" { print $2 }' "$ARTIFACTS")" == "$notes_id" ]] || exit 1 + prior_signed_id=$(awk -F '\t' '$1 == "plugin-distribution" && $3 == "false" { print $2 }' "$ARTIFACTS") + prior_notes_id=$(awk -F '\t' '$1 == "release-notes" && $3 == "false" { print $2 }' "$ARTIFACTS") + [[ "$prior_signed_id" == "$signed_id" && "$prior_notes_id" == "$notes_id" ]] || exit 1 gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name plugin-distribution \ --dir "$RUNNER_TEMP/prior-signed" gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name release-notes \ --dir "$RUNNER_TEMP/prior-notes" [[ -f "$RUNNER_TEMP/prior-signed/${ARCHIVE##*/}" && ! -L "$RUNNER_TEMP/prior-signed/${ARCHIVE##*/}" ]] || exit 1 - [[ -f "$RUNNER_TEMP/prior-notes/release-notes.md" && ! -L "$RUNNER_TEMP/prior-notes/release-notes.md" ]] || exit 1 + [[ -f "$RUNNER_TEMP/prior-notes/release-notes.md" && + ! -L "$RUNNER_TEMP/prior-notes/release-notes.md" ]] || exit 1 test "$(sha256sum "$RUNNER_TEMP/prior-signed/${ARCHIVE##*/}" | cut -d' ' -f1)" = "$EXPECTED_SHA" cmp "$NOTES_FILE" "$RUNNER_TEMP/prior-notes/release-notes.md" - name: Upload signed ZIP diff --git a/pylock.toml b/pylock.toml index 39ff55a..f136c08 100644 --- a/pylock.toml +++ b/pylock.toml @@ -1,53 +1,42 @@ lock-version = "1.0" -environments = [ - "python_version == \"3.15\" and sys_platform == \"linux\" and platform_machine == \"x86_64\" and implementation_name == \"cpython\" and os_name == \"posix\" and platform_python_implementation == \"CPython\" and platform_system == \"Linux\"", - "python_version == \"3.15\" and sys_platform == \"linux\" and platform_machine == \"aarch64\" and implementation_name == \"cpython\" and os_name == \"posix\" and platform_python_implementation == \"CPython\" and platform_system == \"Linux\"", - "python_version == \"3.15\" and sys_platform == \"darwin\" and platform_machine == \"x86_64\" and implementation_name == \"cpython\" and os_name == \"posix\" and platform_python_implementation == \"CPython\" and platform_system == \"Darwin\"", - "python_version == \"3.15\" and sys_platform == \"darwin\" and platform_machine == \"arm64\" and implementation_name == \"cpython\" and os_name == \"posix\" and platform_python_implementation == \"CPython\" and platform_system == \"Darwin\"", - "python_version == \"3.15\" and sys_platform == \"win32\" and platform_machine == \"AMD64\" and implementation_name == \"cpython\" and os_name == \"nt\" and platform_python_implementation == \"CPython\" and platform_system == \"Windows\"", - "python_version == \"3.15\" and sys_platform == \"win32\" and platform_machine == \"ARM64\" and implementation_name == \"cpython\" and os_name == \"nt\" and platform_python_implementation == \"CPython\" and platform_system == \"Windows\"", -] +created-by = "release-creator (uv)" requires-python = ">=3.15" +extras = [] dependency-groups = [ + "project-runtime", "build", - "lock-reader", "lock-tools", - "project-runtime", - "release-attestations", - "release-check", - "release-hook-check", - "release-hooks", - "release-news", + "lock-reader", "release-notes", + "release-news", "release-prepare", - "release-shell", "release-validate", + "release-check", + "release-attestations", + "release-shell", + "release-hooks", + "release-hook-check", + "test", ] -default-groups = [ - "project-runtime", -] -created-by = "nab" +default-groups = [] [[packages]] name = "annotated-types" version = "0.8.0" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "annotated_types-0.8.0.tar.gz" -upload-time = 2026-07-23 20:16:13.995706+00:00 url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz" +upload-time = 2026-07-23 20:16:13+00:00 size = 15893 [packages.sdist.hashes] sha256 = "13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7" [[packages.wheels]] -name = "annotated_types-0.8.0-py3-none-any.whl" -upload-time = 2026-07-23 20:16:12.938651+00:00 url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl" +upload-time = 2026-07-23 20:16:12+00:00 size = 13427 [packages.wheels.hashes] @@ -57,77 +46,107 @@ sha256 = "f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0" name = "argcomplete" version = "3.7.2" marker = "\"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups" -requires-python = ">=3.10" index = "https://pypi.org/simple/" [packages.sdist] -name = "argcomplete-3.7.2.tar.gz" -upload-time = 2026-08-06 04:53:21.662042+00:00 url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz" +upload-time = 2026-08-06 04:53:21+00:00 size = 74473 [packages.sdist.hashes] sha256 = "aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9" [[packages.wheels]] -name = "argcomplete-3.7.2-py3-none-any.whl" -upload-time = 2026-08-06 04:53:20.246664+00:00 url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl" +upload-time = 2026-08-06 04:53:20+00:00 size = 43294 [packages.wheels.hashes] sha256 = "6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19" +[[packages]] +name = "attrs" +version = "26.1.0" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz" +upload-time = 2026-03-19 14:22:25+00:00 +size = 952055 + +[packages.sdist.hashes] +sha256 = "d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl" +upload-time = 2026-03-19 14:22:23+00:00 +size = 67548 + +[packages.wheels.hashes] +sha256 = "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" + [[packages]] name = "build" version = "1.6.1" marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "colorama" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, -] index = "https://pypi.org/simple/" [packages.sdist] -name = "build-1.6.1.tar.gz" -upload-time = 2026-09-10 07:56:00.417084+00:00 url = "https://files.pythonhosted.org/packages/bd/67/4898a44ea4f3f8e213b0954ec0aa0a16971d62a6212d6ea3931e97115b99/build-1.6.1.tar.gz" +upload-time = 2026-09-10 07:56:00+00:00 size = 113427 [packages.sdist.hashes] sha256 = "51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53" [[packages.wheels]] -name = "build-1.6.1-py3-none-any.whl" -upload-time = 2026-09-10 07:55:59.148398+00:00 url = "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl" +upload-time = 2026-09-10 07:55:59+00:00 size = 31179 [packages.wheels.hashes] sha256 = "ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7" +[[packages]] +name = "cachetools" +version = "7.2.2" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/85/e0/27b467a38de29583a68973edbf99eeeee5b105567bdbd7d208e5b07525a7/cachetools-7.2.2.tar.gz" +upload-time = 2026-10-15 20:46:15+00:00 +size = 42916 + +[packages.sdist.hashes] +sha256 = "d521dc98d501ba9efd8d2b1663bbf88163509c0c0a090eea6dc154b421ec5410" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/eb/d1/15cc3b3c07cfe11bf6fbe81a4a92ebb9d202d7d903aaa36b713c8f6eabda/cachetools-7.2.2-py3-none-any.whl" +upload-time = 2026-10-15 20:46:14+00:00 +size = 17819 + +[packages.wheels.hashes] +sha256 = "39b6c9291adde28c5de6622d25329375fadf3e8a678226e7872e2e72854e4549" + [[packages]] name = "certifi" version = "2026.7.22" -marker = "\"lock-reader\" in dependency_groups or \"release-attestations\" in dependency_groups" -requires-python = ">=3.7" +marker = "\"lock-reader\" in dependency_groups or \"release-attestations\" in dependency_groups or \"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "certifi-2026.7.22.tar.gz" -upload-time = 2026-07-22 03:35:12.644458+00:00 url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz" +upload-time = 2026-07-22 03:35:12+00:00 size = 138112 [packages.sdist.hashes] sha256 = "741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55" [[packages.wheels]] -name = "certifi-2026.7.22-py3-none-any.whl" -upload-time = 2026-07-22 03:35:11.276376+00:00 url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl" +upload-time = 2026-07-22 03:35:11+00:00 size = 136983 [packages.wheels.hashes] @@ -136,2317 +155,4668 @@ sha256 = "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775" [[packages]] name = "cffi" version = "2.1.1" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "pycparser" }, -] +marker = "(\"release-attestations\" in dependency_groups and (platform_python_implementation != 'PyPy')) or (\"test\" in dependency_groups and (platform_python_implementation != 'PyPy'))" index = "https://pypi.org/simple/" [packages.sdist] -name = "cffi-2.1.1.tar.gz" -upload-time = 2026-08-03 21:21:18.939988+00:00 url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz" +upload-time = 2026-08-03 21:21:18+00:00 size = 530807 [packages.sdist.hashes] sha256 = "dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be" [[packages.wheels]] -name = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl" -upload-time = 2026-08-03 21:20:50.639490+00:00 +url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl" +upload-time = 2026-08-03 21:20:48+00:00 +size = 194043 + +[packages.wheels.hashes] +sha256 = "dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl" +upload-time = 2026-08-03 21:20:49+00:00 +size = 196737 + +[packages.wheels.hashes] +sha256 = "da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b" + +[[packages.wheels]] url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl" +upload-time = 2026-08-03 21:20:50+00:00 size = 184933 [packages.wheels.hashes] sha256 = "fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4" [[packages.wheels]] -name = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl" -upload-time = 2026-08-03 21:20:52.173342+00:00 url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl" +upload-time = 2026-08-03 21:20:52+00:00 size = 185002 [packages.wheels.hashes] sha256 = "2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8" [[packages.wheels]] -name = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" -upload-time = 2026-08-03 21:20:53.462549+00:00 url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" +upload-time = 2026-08-03 21:20:53+00:00 size = 222271 [packages.wheels.hashes] sha256 = "507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6" [[packages.wheels]] -name = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" -upload-time = 2026-08-03 21:20:57.336082+00:00 +url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl" +upload-time = 2026-08-03 21:20:54+00:00 +size = 209919 + +[packages.wheels.hashes] +sha256 = "246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl" +upload-time = 2026-08-03 21:20:56+00:00 +size = 208529 + +[packages.wheels.hashes] +sha256 = "471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779" + +[[packages.wheels]] url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-08-03 21:20:57+00:00 size = 221630 [packages.wheels.hashes] sha256 = "aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399" [[packages.wheels]] -name = "cffi-2.1.1-cp315-cp315-win_amd64.whl" -upload-time = 2026-08-03 21:21:16.108179+00:00 +url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl" +upload-time = 2026-08-03 21:20:58+00:00 +size = 225134 + +[packages.wheels.hashes] +sha256 = "42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl" +upload-time = 2026-08-03 21:20:59+00:00 +size = 223197 + +[packages.wheels.hashes] +sha256 = "cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl" +upload-time = 2026-08-03 21:21:14+00:00 +size = 177683 + +[packages.wheels.hashes] +sha256 = "4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac" + +[[packages.wheels]] url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl" +upload-time = 2026-08-03 21:21:16+00:00 size = 187897 [packages.wheels.hashes] sha256 = "e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960" [[packages.wheels]] -name = "cffi-2.1.1-cp315-cp315-win_arm64.whl" -upload-time = 2026-08-03 21:21:17.271814+00:00 url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl" +upload-time = 2026-08-03 21:21:17+00:00 size = 182935 [packages.wheels.hashes] sha256 = "27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1" -[[packages]] -name = "charset-normalizer" -version = "3.5.2" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.7" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "charset_normalizer-3.5.2.tar.gz" -upload-time = 2026-09-30 04:39:23.398219+00:00 -url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz" -size = 172659 - -[packages.sdist.hashes] -sha256 = "39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef" - [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl" -upload-time = 2026-09-30 04:37:28.329851+00:00 -url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl" -size = 367676 +url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl" +upload-time = 2026-08-03 21:21:01+00:00 +size = 188464 [packages.wheels.hashes] -sha256 = "87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26" +sha256 = "c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -upload-time = 2026-09-30 04:37:30.167187+00:00 -url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -size = 248414 +url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl" +upload-time = 2026-08-03 21:21:02+00:00 +size = 188262 [packages.wheels.hashes] -sha256 = "9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364" +sha256 = "4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" -upload-time = 2026-09-30 04:37:37.397915+00:00 -url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" -size = 257336 +url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" +upload-time = 2026-08-03 21:21:03+00:00 +size = 223779 [packages.wheels.hashes] -sha256 = "3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229" +sha256 = "937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl" -upload-time = 2026-09-30 04:37:53.427053+00:00 -url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl" -size = 210858 +url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl" +upload-time = 2026-08-03 21:21:04+00:00 +size = 211520 [packages.wheels.hashes] -sha256 = "89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8" +sha256 = "df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl" -upload-time = 2026-09-30 04:37:55.015729+00:00 -url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl" -size = 200309 +url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl" +upload-time = 2026-08-03 21:21:06+00:00 +size = 210673 [packages.wheels.hashes] -sha256 = "6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a" +sha256 = "a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl" -upload-time = 2026-09-30 04:38:26.216534+00:00 -url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl" -size = 331815 +url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-08-03 21:21:07+00:00 +size = 223835 [packages.wheels.hashes] -sha256 = "4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd" +sha256 = "363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" -upload-time = 2026-09-30 04:38:28.032254+00:00 -url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" -size = 253276 +url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl" +upload-time = 2026-08-03 21:21:08+00:00 +size = 226705 [packages.wheels.hashes] -sha256 = "1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7" +sha256 = "770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -upload-time = 2026-09-30 04:38:29.732000+00:00 -url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -size = 241239 +url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl" +upload-time = 2026-08-03 21:21:09+00:00 +size = 225539 [packages.wheels.hashes] -sha256 = "d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f" +sha256 = "7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl" -upload-time = 2026-09-30 04:38:49.707190+00:00 -url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl" -size = 200092 +url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl" +upload-time = 2026-08-03 21:21:11+00:00 +size = 182707 [packages.wheels.hashes] -sha256 = "9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036" +sha256 = "06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66" [[packages.wheels]] -name = "charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl" -upload-time = 2026-09-30 04:38:51.312480+00:00 -url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl" -size = 294363 +url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl" +upload-time = 2026-08-03 21:21:12+00:00 +size = 193772 [packages.wheels.hashes] -sha256 = "40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e" +sha256 = "d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3" [[packages.wheels]] -name = "charset_normalizer-3.5.2-py3-none-any.whl" -upload-time = 2026-09-30 04:39:21.828049+00:00 -url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl" -size = 68872 +url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl" +upload-time = 2026-08-03 21:21:13+00:00 +size = 186360 [packages.wheels.hashes] -sha256 = "b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685" +sha256 = "d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692" [[packages]] -name = "click" -version = "8.5.0" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.10" +name = "charset-normalizer" +version = "3.5.2" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "click-8.5.0.tar.gz" -upload-time = 2026-08-26 13:33:14.560531+00:00 -url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz" -size = 382235 +url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz" +upload-time = 2026-09-30 04:39:23+00:00 +size = 172659 [packages.sdist.hashes] -sha256 = "ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34" +sha256 = "39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef" [[packages.wheels]] -name = "click-8.5.0-py3-none-any.whl" -upload-time = 2026-08-26 13:33:12.928043+00:00 -url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl" -size = 125251 +url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl" +upload-time = 2026-09-30 04:37:28+00:00 +size = 367676 [packages.wheels.hashes] -sha256 = "255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360" +sha256 = "87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26" -[[packages]] -name = "colorama" -version = "0.4.6" -marker = "\"lock-tools\" in dependency_groups and sys_platform == \"win32\"" -requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -index = "https://pypi.org/simple/" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-09-30 04:37:30+00:00 +size = 248414 -[packages.sdist] -name = "colorama-0.4.6.tar.gz" -upload-time = 2022-10-25 02:36:22.414799+00:00 -url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz" -size = 27697 +[packages.wheels.hashes] +sha256 = "9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364" -[packages.sdist.hashes] -sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" +upload-time = 2026-09-30 04:37:31+00:00 +size = 244775 + +[packages.wheels.hashes] +sha256 = "498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253" [[packages.wheels]] -name = "colorama-0.4.6-py2.py3-none-any.whl" -upload-time = 2022-10-25 02:36:20.889702+00:00 -url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl" -size = 25335 +url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" +upload-time = 2026-09-30 04:37:33+00:00 +size = 273877 [packages.wheels.hashes] -sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" +sha256 = "e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0" -[[packages]] -name = "cryptography" -version = "50.0.2" -marker = "\"release-attestations\" in dependency_groups" -requires-python = "!=3.9.0,!=3.9.1,>=3.9" -dependencies = [ - { name = "cffi" }, -] -index = "https://pypi.org/simple/" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" +upload-time = 2026-09-30 04:37:35+00:00 +size = 269455 -[packages.sdist] -name = "cryptography-50.0.2.tar.gz" -upload-time = 2026-09-30 15:30:04.884601+00:00 -url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz" -size = 880623 +[packages.wheels.hashes] +sha256 = "def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc" -[packages.sdist.hashes] -sha256 = "7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 04:37:37+00:00 +size = 257336 + +[packages.wheels.hashes] +sha256 = "3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229" [[packages.wheels]] -name = "cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl" -upload-time = 2026-09-30 14:43:44.339921+00:00 -url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl" -size = 3914904 +url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" +upload-time = 2026-09-30 04:37:39+00:00 +size = 257584 [packages.wheels.hashes] -sha256 = "fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb" +sha256 = "04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5" [[packages.wheels]] -name = "cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" -upload-time = 2026-09-30 14:43:47.113016+00:00 -url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" -size = 4731146 +url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-30 04:37:41+00:00 +size = 251854 [packages.wheels.hashes] -sha256 = "79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0" +sha256 = "183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98" [[packages.wheels]] -name = "cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" -upload-time = 2026-09-30 14:43:49.010540+00:00 -url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" -size = 4719841 +url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl" +upload-time = 2026-09-30 04:37:42+00:00 +size = 248767 [packages.wheels.hashes] -sha256 = "630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2" +sha256 = "16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3" [[packages.wheels]] -name = "cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl" -upload-time = 2026-09-30 14:43:50.932452+00:00 -url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl" -size = 4738340 +url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl" +upload-time = 2026-09-30 04:37:44+00:00 +size = 274479 [packages.wheels.hashes] -sha256 = "f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480" +sha256 = "7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2" [[packages.wheels]] -name = "cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl" -upload-time = 2026-09-30 14:43:55.272150+00:00 -url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl" -size = 4753050 +url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl" +upload-time = 2026-09-30 04:37:46+00:00 +size = 258310 [packages.wheels.hashes] -sha256 = "4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856" +sha256 = "ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf" [[packages.wheels]] -name = "cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl" -upload-time = 2026-09-30 14:43:59.541693+00:00 -url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl" -size = 4737859 +url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl" +upload-time = 2026-09-30 04:37:48+00:00 +size = 271100 [packages.wheels.hashes] -sha256 = "87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04" +sha256 = "df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95" [[packages.wheels]] -name = "cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl" -upload-time = 2026-09-30 14:44:04.545137+00:00 -url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl" -size = 4752576 +url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-30 04:37:50+00:00 +size = 259936 [packages.wheels.hashes] -sha256 = "9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079" +sha256 = "f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d" [[packages.wheels]] -name = "cryptography-50.0.2-cp311-abi3-win_amd64.whl" -upload-time = 2026-09-30 14:44:11.671809+00:00 -url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl" -size = 3824692 +url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl" +upload-time = 2026-09-30 04:37:51+00:00 +size = 186301 [packages.wheels.hashes] -sha256 = "7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c" +sha256 = "281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847" [[packages.wheels]] -name = "cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl" -upload-time = 2026-09-30 14:44:41.807195+00:00 -url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl" -size = 4133708 +url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl" +upload-time = 2026-09-30 04:37:53+00:00 +size = 210858 [packages.wheels.hashes] -sha256 = "edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37" +sha256 = "89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8" [[packages.wheels]] -name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" -upload-time = 2026-09-30 14:44:43.693364+00:00 -url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" -size = 4956267 +url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl" +upload-time = 2026-09-30 04:37:55+00:00 +size = 200309 [packages.wheels.hashes] -sha256 = "d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a" +sha256 = "6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a" [[packages.wheels]] -name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" -upload-time = 2026-09-30 14:44:45.769143+00:00 -url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" -size = 4966465 +url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl" +upload-time = 2026-09-30 04:37:56+00:00 +size = 395343 [packages.wheels.hashes] -sha256 = "f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67" +sha256 = "915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1" [[packages.wheels]] -name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl" -upload-time = 2026-09-30 14:44:48.211032+00:00 -url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl" -size = 4959356 +url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-09-30 04:37:58+00:00 +size = 250114 [packages.wheels.hashes] -sha256 = "e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc" +sha256 = "65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b" [[packages.wheels]] -name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl" -upload-time = 2026-09-30 14:44:53.379605+00:00 -url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl" -size = 5001199 +url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" +upload-time = 2026-09-30 04:38:00+00:00 +size = 239997 [packages.wheels.hashes] -sha256 = "58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7" +sha256 = "b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f" [[packages.wheels]] -name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl" -upload-time = 2026-09-30 14:44:59.639056+00:00 -url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl" -size = 4958822 +url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" +upload-time = 2026-09-30 04:38:02+00:00 +size = 272313 [packages.wheels.hashes] -sha256 = "fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b" +sha256 = "e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3" [[packages.wheels]] -name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl" -upload-time = 2026-09-30 14:45:05.009031+00:00 -url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl" -size = 5000859 +url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" +upload-time = 2026-09-30 04:38:04+00:00 +size = 269986 [packages.wheels.hashes] -sha256 = "e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c" +sha256 = "124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e" [[packages.wheels]] -name = "cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl" -upload-time = 2026-09-30 15:29:20.155896+00:00 -url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl" -size = 4111557 +url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 04:38:06+00:00 +size = 260202 [packages.wheels.hashes] -sha256 = "c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c" +sha256 = "28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9" [[packages.wheels]] -name = "cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl" -upload-time = 2026-09-30 15:29:22.265815+00:00 -url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl" -size = 3943588 +url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" +upload-time = 2026-09-30 04:38:08+00:00 +size = 259083 [packages.wheels.hashes] -sha256 = "0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2" +sha256 = "58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a" [[packages.wheels]] -name = "cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" -upload-time = 2026-09-30 15:29:24.580797+00:00 -url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" -size = 4756166 +url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-30 04:38:10+00:00 +size = 253081 [packages.wheels.hashes] -sha256 = "c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd" +sha256 = "443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115" [[packages.wheels]] -name = "cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" -upload-time = 2026-09-30 15:29:26.807001+00:00 -url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" -size = 4749145 +url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl" +upload-time = 2026-09-30 04:38:12+00:00 +size = 248712 [packages.wheels.hashes] -sha256 = "ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767" +sha256 = "58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c" [[packages.wheels]] -name = "cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl" -upload-time = 2026-09-30 15:29:28.588698+00:00 -url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl" -size = 4763638 +url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl" +upload-time = 2026-09-30 04:38:14+00:00 +size = 274176 [packages.wheels.hashes] -sha256 = "dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454" +sha256 = "1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d" [[packages.wheels]] -name = "cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl" -upload-time = 2026-09-30 15:29:32.605292+00:00 -url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl" -size = 4781387 +url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl" +upload-time = 2026-09-30 04:38:15+00:00 +size = 259413 [packages.wheels.hashes] -sha256 = "f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5" +sha256 = "ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d" [[packages.wheels]] -name = "cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl" -upload-time = 2026-09-30 15:29:36.149639+00:00 -url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl" -size = 4764319 +url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl" +upload-time = 2026-09-30 04:38:17+00:00 +size = 269434 [packages.wheels.hashes] -sha256 = "0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602" +sha256 = "5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4" [[packages.wheels]] -name = "cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl" -upload-time = 2026-09-30 15:29:41.251208+00:00 -url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl" -size = 4780973 +url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-30 04:38:19+00:00 +size = 262716 [packages.wheels.hashes] -sha256 = "1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c" +sha256 = "51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b" [[packages.wheels]] -name = "cryptography-50.0.2-cp39-abi3-win_amd64.whl" -upload-time = 2026-09-30 15:29:46.782322+00:00 -url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl" -size = 3854095 +url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl" +upload-time = 2026-09-30 04:38:21+00:00 +size = 197159 [packages.wheels.hashes] -sha256 = "4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de" +sha256 = "5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800" -[[packages]] -name = "cython" -version = "3.3.0" -marker = "\"build\" in dependency_groups" -requires-python = ">=3.9" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "cython-3.3.0.tar.gz" -upload-time = 2026-08-22 05:16:39.493040+00:00 -url = "https://files.pythonhosted.org/packages/a9/d8/4981ef716ad0e3ff0d3ef383aefc6b03c4a88dee33b272bf8e0d833001ca/cython-3.3.0.tar.gz" -size = 3727515 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl" +upload-time = 2026-09-30 04:38:22+00:00 +size = 221322 -[packages.sdist.hashes] -sha256 = "eed0d93fbca7087f143b42c34b05a825849bdf17f101572c2105acfa49aa88b8" +[packages.wheels.hashes] +sha256 = "a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21" [[packages.wheels]] -name = "cython-3.3.0-cp315-cp315-macosx_11_0_arm64.whl" -upload-time = 2026-08-22 05:17:32.523944+00:00 -url = "https://files.pythonhosted.org/packages/ea/45/7f6988070013e16918e39b1b3dab9c5f2c8e404253a7fd10ee685bbd6902/cython-3.3.0-cp315-cp315-macosx_11_0_arm64.whl" -size = 3177681 +url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl" +upload-time = 2026-09-30 04:38:24+00:00 +size = 209192 [packages.wheels.hashes] -sha256 = "596e8df019372a2cd417805015022d42cb8ee4e1803ccdc11ed00e451625fb66" +sha256 = "749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58" [[packages.wheels]] -name = "cython-3.3.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -upload-time = 2026-08-22 05:17:34.511844+00:00 -url = "https://files.pythonhosted.org/packages/6e/5d/afb6866ab10236bb208dff0f172ea4b397c9693c5250280c4d9d26057218/cython-3.3.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -size = 3328312 +url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl" +upload-time = 2026-09-30 04:38:26+00:00 +size = 331815 [packages.wheels.hashes] -sha256 = "4a36c34d1950845b8ac148653b07cdc62421a4b0d9abfcc849e69f1c4ff9919d" +sha256 = "4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd" [[packages.wheels]] -name = "cython-3.3.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" -upload-time = 2026-08-22 05:17:36.538454+00:00 -url = "https://files.pythonhosted.org/packages/c9/aa/4c0b6773ecf6bc1ec6cda7db8312a566611b330fb6dae87d740e44a47822/cython-3.3.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" -size = 3454285 +url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" +upload-time = 2026-09-30 04:38:28+00:00 +size = 253276 [packages.wheels.hashes] -sha256 = "b447f6906e0555f05dc4742ef1f99091b1e5d9aa9f16616e772fbf9ff6271616" +sha256 = "1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7" [[packages.wheels]] -name = "cython-3.3.0-cp315-cp315-win_amd64.whl" -upload-time = 2026-08-22 05:17:38.650619+00:00 -url = "https://files.pythonhosted.org/packages/44/bb/3e2631122f96300723d6fc42b9cf65550bdcda570a6ad5c4e0226e2e787c/cython-3.3.0-cp315-cp315-win_amd64.whl" -size = 2924709 +url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-09-30 04:38:29+00:00 +size = 241239 [packages.wheels.hashes] -sha256 = "b55c72e8eccdd508c8de3cf3bbc543aafbb3bf6a518e1ee20358d3241cd780ef" +sha256 = "d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f" [[packages.wheels]] -name = "cython-3.3.0-cp39-abi3-macosx_10_9_x86_64.whl" -upload-time = 2026-08-22 05:17:40.923615+00:00 -url = "https://files.pythonhosted.org/packages/14/59/bc1a84b434cb5bebb0cd6f50da8f239d35a5c141b20fdeafc2817fd87778/cython-3.3.0-cp39-abi3-macosx_10_9_x86_64.whl" -size = 3063660 +url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" +upload-time = 2026-09-30 04:38:31+00:00 +size = 231121 [packages.wheels.hashes] -sha256 = "e0d2713d2b292c826bc21dc8732bd9e47628103aa3764180c881e04b3fef95dc" +sha256 = "0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93" [[packages.wheels]] -name = "cython-3.3.0-cp39-abi3-win_arm64.whl" -upload-time = 2026-08-22 05:17:56.386837+00:00 -url = "https://files.pythonhosted.org/packages/19/a2/5f4d305cbd4489d21570e5491ad5c483c478cdab032853e2125c280e3bd5/cython-3.3.0-cp39-abi3-win_arm64.whl" -size = 2608078 +url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" +upload-time = 2026-09-30 04:38:33+00:00 +size = 260350 [packages.wheels.hashes] -sha256 = "dce56c26d388f00a19426371b6926bf2f77c5c03b71d5273e4556c68be98c2dd" +sha256 = "838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade" [[packages.wheels]] -name = "cython-3.3.0-py3-none-any.whl" -upload-time = 2026-08-22 05:16:36.741632+00:00 -url = "https://files.pythonhosted.org/packages/bf/77/67b0b24e45073a699610e50f00c18474ff9b09ea29ecc95083bdf5e60acd/cython-3.3.0-py3-none-any.whl" -size = 1349151 +url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" +upload-time = 2026-09-30 04:38:34+00:00 +size = 255430 [packages.wheels.hashes] -sha256 = "9b24b5c8cd536946b62086fcafee6d5509d3f549f72d553d2336af87ffbe0da1" +sha256 = "2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0" -[[packages]] -name = "dnspython" -version = "2.9.0" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.11" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "dnspython-2.9.0.tar.gz" -upload-time = 2026-10-09 00:07:24.352654+00:00 -url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz" -size = 423560 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" +upload-time = 2026-09-30 04:38:36+00:00 +size = 250612 -[packages.sdist.hashes] -sha256 = "b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1" +[packages.wheels.hashes] +sha256 = "0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26" [[packages.wheels]] -name = "dnspython-2.9.0-py3-none-any.whl" -upload-time = 2026-10-09 00:07:22.622887+00:00 -url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl" -size = 354822 +url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-30 04:38:38+00:00 +size = 242083 [packages.wheels.hashes] -sha256 = "9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9" +sha256 = "1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011" -[[packages]] -name = "docutils" -version = "0.23" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.9" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "docutils-0.23.tar.gz" -upload-time = 2026-05-27 17:41:06.934109+00:00 -url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz" -size = 2303823 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl" +upload-time = 2026-09-30 04:38:39+00:00 +size = 232738 -[packages.sdist.hashes] -sha256 = "746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e" +[packages.wheels.hashes] +sha256 = "304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621" [[packages.wheels]] -name = "docutils-0.23-py3-none-any.whl" -upload-time = 2026-05-27 17:40:58.442528+00:00 -url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl" -size = 634701 +url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl" +upload-time = 2026-09-30 04:38:41+00:00 +size = 260703 [packages.wheels.hashes] -sha256 = "25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea" +sha256 = "9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4" -[[packages]] -name = "email-validator" -version = "2.3.0" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.8" -dependencies = [ - { name = "dnspython" }, - { name = "idna" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "email_validator-2.3.0.tar.gz" -upload-time = 2025-08-26 13:09:06.831581+00:00 -url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz" -size = 51238 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl" +upload-time = 2026-09-30 04:38:42+00:00 +size = 247622 -[packages.sdist.hashes] -sha256 = "9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426" +[packages.wheels.hashes] +sha256 = "01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e" [[packages.wheels]] -name = "email_validator-2.3.0-py3-none-any.whl" -upload-time = 2025-08-26 13:09:05.858779+00:00 -url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl" -size = 35604 +url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl" +upload-time = 2026-09-30 04:38:44+00:00 +size = 257500 [packages.wheels.hashes] -sha256 = "80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4" - -[[packages]] -name = "id" -version = "1.6.1" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.9" -dependencies = [ - { name = "urllib3" }, -] -index = "https://pypi.org/simple/" +sha256 = "7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c" -[packages.sdist] -name = "id-1.6.1.tar.gz" -upload-time = 2026-02-04 16:19:41.260133+00:00 -url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz" -size = 18088 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-30 04:38:46+00:00 +size = 255100 -[packages.sdist.hashes] -sha256 = "d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069" +[packages.wheels.hashes] +sha256 = "9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0" [[packages.wheels]] -name = "id-1.6.1-py3-none-any.whl" -upload-time = 2026-02-04 16:19:40.051745+00:00 -url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl" -size = 14689 +url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl" +upload-time = 2026-09-30 04:38:47+00:00 +size = 174499 [packages.wheels.hashes] -sha256 = "f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca" +sha256 = "01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf" -[[packages]] -name = "idna" -version = "3.20" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.9" -index = "https://pypi.org/simple/" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl" +upload-time = 2026-09-30 04:38:49+00:00 +size = 200092 -[packages.sdist] -name = "idna-3.20.tar.gz" -upload-time = 2026-09-17 14:11:04.752193+00:00 -url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz" -size = 216463 +[packages.wheels.hashes] +sha256 = "9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036" -[packages.sdist.hashes] -sha256 = "a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl" +upload-time = 2026-09-30 04:38:51+00:00 +size = 294363 + +[packages.wheels.hashes] +sha256 = "40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e" [[packages.wheels]] -name = "idna-3.20-py3-none-any.whl" -upload-time = 2026-09-17 14:11:03.168153+00:00 -url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl" -size = 69583 +url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl" +upload-time = 2026-09-30 04:39:21+00:00 +size = 68872 [packages.wheels.hashes] -sha256 = "ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c" +sha256 = "b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685" [[packages]] -name = "installer" -version = "1.1.0" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" +name = "check-wheel-contents" +version = "0.6.3" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "installer-1.1.0.tar.gz" -upload-time = 2026-10-12 19:35:52.851170+00:00 -url = "https://files.pythonhosted.org/packages/56/d3/2fab33d15004d41ae8e7b5d3e310510f7f34a7e1dd032a3e60d8b38d8fba/installer-1.1.0.tar.gz" -size = 481257 +url = "https://files.pythonhosted.org/packages/95/37/2b9e0d38c2f668791ffe8b97711185c364d91c027e47f189c371c2348d18/check_wheel_contents-0.6.3.tar.gz" +upload-time = 2025-08-02 14:01:45+00:00 +size = 586023 [packages.sdist.hashes] -sha256 = "5adc0c01ce62d809989e246987cd68944fe5df8af562d0358f65b2ab15cb5de7" +sha256 = "10e6939e2fe4e6ce1edf2ff6ec6157808677e80782e78021ae139dd88473a442" [[packages.wheels]] -name = "installer-1.1.0-py3-none-any.whl" -upload-time = 2026-10-12 19:35:51.096942+00:00 -url = "https://files.pythonhosted.org/packages/d7/7a/63547a792a12148882205d79e07644091d5b7690f40c2bb9d41452a8bdc2/installer-1.1.0-py3-none-any.whl" -size = 464466 +url = "https://files.pythonhosted.org/packages/be/05/f39fde9f31ef80b285ef5822fad4ddabf73fec62a1f02c5beb4b2f328972/check_wheel_contents-0.6.3-py3-none-any.whl" +upload-time = 2025-08-02 14:01:43+00:00 +size = 27541 [packages.wheels.hashes] -sha256 = "f55f5728ff61d3dd54dbc5a397cf3f7307cb64b180c20b72afbebf01c9e2c8a0" +sha256 = "5ae39c8c434b972f0740d04610759168590713175aab584b012b1b84f6771874" [[packages]] -name = "jinja2" -version = "3.1.6" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.7" -dependencies = [ - { name = "markupsafe" }, -] +name = "click" +version = "8.5.0" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups or \"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "jinja2-3.1.6.tar.gz" -upload-time = 2025-03-05 20:05:02.478302+00:00 -url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz" -size = 245115 +url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz" +upload-time = 2026-08-26 13:33:14+00:00 +size = 382235 [packages.sdist.hashes] -sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d" +sha256 = "ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34" [[packages.wheels]] -name = "jinja2-3.1.6-py3-none-any.whl" -upload-time = 2025-03-05 20:05:00.369721+00:00 -url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl" -size = 134899 +url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl" +upload-time = 2026-08-26 13:33:12+00:00 +size = 125251 [packages.wheels.hashes] -sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" +sha256 = "255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360" [[packages]] -name = "markdown-it-py" -version = "4.2.0" -marker = "\"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "mdurl" }, -] +name = "colorama" +version = "0.4.6" +marker = "\"test\" in dependency_groups or (\"lock-tools\" in dependency_groups and (os_name == 'nt'))" index = "https://pypi.org/simple/" [packages.sdist] -name = "markdown_it_py-4.2.0.tar.gz" -upload-time = 2026-05-07 12:08:28.360612+00:00 -url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz" -size = 82454 +url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz" +upload-time = 2022-10-25 02:36:22+00:00 +size = 27697 [packages.sdist.hashes] -sha256 = "04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49" +sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" [[packages.wheels]] -name = "markdown_it_py-4.2.0-py3-none-any.whl" -upload-time = 2026-05-07 12:08:27.182789+00:00 -url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl" -size = 91687 +url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl" +upload-time = 2022-10-25 02:36:20+00:00 +size = 25335 [packages.wheels.hashes] -sha256 = "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" +sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" [[packages]] -name = "markupsafe" -version = "3.0.4" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.9" +name = "coverage" +version = "7.16.2" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "markupsafe-3.0.4.tar.gz" -upload-time = 2026-10-02 23:07:22.290270+00:00 -url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz" -size = 153777 +url = "https://files.pythonhosted.org/packages/2f/55/d1eaf3e73781174340a00dc1ba2aee8a65f82fadb18e2797b192b6b3925b/coverage-7.16.2.tar.gz" +upload-time = 2026-09-27 12:29:01+00:00 +size = 971999 [packages.sdist.hashes] -sha256 = "2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6" +sha256 = "ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa" [[packages.wheels]] -name = "markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl" -upload-time = 2026-10-02 23:06:16.701372+00:00 -url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl" -size = 11719 +url = "https://files.pythonhosted.org/packages/4c/57/52935003c3f627ba6e5203d7179aad32448c10899663a30336aba8e81a2c/coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl" +upload-time = 2026-09-27 12:27:59+00:00 +size = 224140 [packages.wheels.hashes] -sha256 = "799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8" +sha256 = "414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8" [[packages.wheels]] -name = "markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl" -upload-time = 2026-10-02 23:06:17.855823+00:00 -url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl" -size = 12075 +url = "https://files.pythonhosted.org/packages/31/38/df472520f3e626524d7e2fc9d6da0afe7895a2f1489d36b48af8ca40bb41/coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl" +upload-time = 2026-09-27 12:28:01+00:00 +size = 224449 [packages.wheels.hashes] -sha256 = "ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97" +sha256 = "00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24" [[packages.wheels]] -name = "markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -upload-time = 2026-10-02 23:06:18.963942+00:00 -url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -size = 24656 +url = "https://files.pythonhosted.org/packages/0c/aa/3be084d5b82e63ccdad4ed751e4acbae294673573e30481d29f8b7402eec/coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl" +upload-time = 2026-09-27 12:28:03+00:00 +size = 256060 [packages.wheels.hashes] -sha256 = "4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b" +sha256 = "4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09" [[packages.wheels]] -name = "markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" -upload-time = 2026-10-02 23:06:22.524416+00:00 -url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" -size = 23111 +url = "https://files.pythonhosted.org/packages/de/29/48fca82a7ebf7ff7b2e35019cc9537e7f65e4d2aa1215cc5a8792c989251/coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" +upload-time = 2026-09-27 12:28:05+00:00 +size = 259106 [packages.wheels.hashes] -sha256 = "925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369" +sha256 = "8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843" [[packages.wheels]] -name = "markupsafe-3.0.4-cp315-cp315-win_amd64.whl" -upload-time = 2026-10-02 23:06:33.258900+00:00 -url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl" -size = 14496 +url = "https://files.pythonhosted.org/packages/06/3d/b2d5986f2dd53fe201aa1be2e4ab204fa1aed5101e67c0dbbb419b850aee/coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-09-27 12:28:06+00:00 +size = 260674 [packages.wheels.hashes] -sha256 = "d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214" +sha256 = "9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1" [[packages.wheels]] -name = "markupsafe-3.0.4-cp315-cp315-win_arm64.whl" -upload-time = 2026-10-02 23:06:34.539930+00:00 -url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl" -size = 14361 +url = "https://files.pythonhosted.org/packages/ce/7e/b50160be3506ead12e6480d14279af7f0f17627694300a2d1fd2c42d2ff5/coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" +upload-time = 2026-09-27 12:28:08+00:00 +size = 263139 [packages.wheels.hashes] -sha256 = "353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67" - -[[packages]] -name = "mdformat" -version = "1.0.0" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "markdown-it-py" }, -] -index = "https://pypi.org/simple/" +sha256 = "aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982" -[packages.sdist] -name = "mdformat-1.0.0.tar.gz" -upload-time = 2025-10-16 12:05:03.695033+00:00 -url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz" -size = 56953 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/14/5e/7c805ac9a32606de1399bd7e9bd375aa2f973dc61b12680d9e6403c2e891/coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" +upload-time = 2026-09-27 12:28:10+00:00 +size = 256591 -[packages.sdist.hashes] -sha256 = "4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928" +[packages.wheels.hashes] +sha256 = "9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34" [[packages.wheels]] -name = "mdformat-1.0.0-py3-none-any.whl" -upload-time = 2025-10-16 12:05:02.607432+00:00 -url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl" -size = 53288 +url = "https://files.pythonhosted.org/packages/ab/9e/76f1ed129a2daf658a3ea17122824cf2e3b91fea0460d8d3664fc5a61018/coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-27 12:28:12+00:00 +size = 258708 [packages.wheels.hashes] -sha256 = "bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b" +sha256 = "80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6" -[[packages]] -name = "mdformat-gfm" -version = "1.0.0" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "markdown-it-py" }, - { name = "mdformat" }, - { name = "mdit-py-plugins" }, - { name = "wcwidth" }, -] -index = "https://pypi.org/simple/" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5a/b7/8d62e75f48b527619239a65294f842d4b7fd02a0839d43ae1de80184e2df/coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl" +upload-time = 2026-09-27 12:28:14+00:00 +size = 256562 -[packages.sdist] -name = "mdformat_gfm-1.0.0.tar.gz" -upload-time = 2025-10-16 09:12:22.402429+00:00 -url = "https://files.pythonhosted.org/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz" -size = 10112 +[packages.wheels.hashes] +sha256 = "7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4" -[packages.sdist.hashes] -sha256 = "d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b8/8d/0a15f95c3afb78e947c52644786ba4bc9de259905687dd720d5e6fae2e76/coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl" +upload-time = 2026-09-27 12:28:17+00:00 +size = 261077 + +[packages.wheels.hashes] +sha256 = "44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0" [[packages.wheels]] -name = "mdformat_gfm-1.0.0-py3-none-any.whl" -upload-time = 2025-10-16 09:12:21.276157+00:00 -url = "https://files.pythonhosted.org/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl" -size = 10970 +url = "https://files.pythonhosted.org/packages/25/00/88389987305a47d732866c07c8a500000ab574df9505e3114ac69c8d027f/coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl" +upload-time = 2026-09-27 12:28:19+00:00 +size = 256034 [packages.wheels.hashes] -sha256 = "7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005" +sha256 = "59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a" -[[packages]] -name = "mdit-py-plugins" -version = "0.6.1" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "markdown-it-py" }, -] -index = "https://pypi.org/simple/" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/92/02/34d079d4952ad461bde037d353f9a6e037a7edc45fe0f9ee8781ff73f028/coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-27 12:28:21+00:00 +size = 258034 -[packages.sdist] -name = "mdit_py_plugins-0.6.1.tar.gz" -upload-time = 2026-05-13 09:03:38.910230+00:00 -url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz" -size = 56114 +[packages.wheels.hashes] +sha256 = "066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63" -[packages.sdist.hashes] -sha256 = "a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f6/d8/3e59a62879285b464ec1b10fd824fbc1af9ce66e842cd39974f80a0becc4/coverage-7.16.2-cp315-cp315-win32.whl" +upload-time = 2026-09-27 12:28:23+00:00 +size = 226252 + +[packages.wheels.hashes] +sha256 = "893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e" [[packages.wheels]] -name = "mdit_py_plugins-0.6.1-py3-none-any.whl" -upload-time = 2026-05-13 09:03:37.760000+00:00 -url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl" -size = 66663 +url = "https://files.pythonhosted.org/packages/f4/e1/128026e1b2836e9ad6b219207ba9edf1c5e0088a7869e23088aee7fbbe7a/coverage-7.16.2-cp315-cp315-win_amd64.whl" +upload-time = 2026-09-27 12:28:25+00:00 +size = 226728 [packages.wheels.hashes] -sha256 = "214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d" +sha256 = "01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a8/f4/c9fa8e7cf525ca7748ac52b0ee89331d13fe09808e45c679830708782e90/coverage-7.16.2-cp315-cp315-win_arm64.whl" +upload-time = 2026-09-27 12:28:27+00:00 +size = 226648 + +[packages.wheels.hashes] +sha256 = "967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a2/13/e96b045447a856666f36f9c653e2a80bdaa732aaaf72412b19aa2c26a473/coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl" +upload-time = 2026-09-27 12:28:29+00:00 +size = 224858 + +[packages.wheels.hashes] +sha256 = "98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/23/90/087f6ad1bd3df059632ca3407a4e6552ed1053ee35354de0a771acf35423/coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl" +upload-time = 2026-09-27 12:28:31+00:00 +size = 225139 + +[packages.wheels.hashes] +sha256 = "3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/7e/8e/285dcef0184358044e7cbcd810a1bdc9566bc620f54702d605477155df4a/coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl" +upload-time = 2026-09-27 12:28:33+00:00 +size = 265090 + +[packages.wheels.hashes] +sha256 = "fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/06/b2/cc83f3a6e5789a4e89059c69555bc641c2efcde568405a1c06fc702951ab/coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" +upload-time = 2026-09-27 12:28:35+00:00 +size = 268254 + +[packages.wheels.hashes] +sha256 = "eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ac/41/f548c19530f5d66ac6e3c92bbcbc49da7261de3a458b9f3e54a3efb1a0b2/coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-09-27 12:28:36+00:00 +size = 270695 + +[packages.wheels.hashes] +sha256 = "444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/94/61/4dc27cf82ef96434d2874110ad0cc10ea4621025705dc5049862bd3bd181/coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" +upload-time = 2026-09-27 12:28:38+00:00 +size = 271853 + +[packages.wheels.hashes] +sha256 = "a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/38/29/bf8072b1b8bd5f2de8b21460a404460b1a2b97e80a9464c78ec0271f6199/coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" +upload-time = 2026-09-27 12:28:40+00:00 +size = 265644 + +[packages.wheels.hashes] +sha256 = "8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/7c/2f/0aecb8721be5cdeb8afd9d6d9f6b463f074e4d8d37f00f4c42442522709f/coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-27 12:28:42+00:00 +size = 268530 + +[packages.wheels.hashes] +sha256 = "396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ab/0b/92b4b7628268ee711249958e68fc0328779bd3d9a7ab4715379465aedb84/coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl" +upload-time = 2026-09-27 12:28:44+00:00 +size = 265154 + +[packages.wheels.hashes] +sha256 = "9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/7b/d9/41c95c1ab29b3dcd357cd1227181d1c98185632aca41ce670ce671b23a43/coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl" +upload-time = 2026-09-27 12:28:46+00:00 +size = 269799 + +[packages.wheels.hashes] +sha256 = "fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/80/07/ebeb259aa5362b033a137b86d7274ff4b109d59be8cc9913889b783bf75a/coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl" +upload-time = 2026-09-27 12:28:48+00:00 +size = 265196 + +[packages.wheels.hashes] +sha256 = "bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b2/18/8437620f90d023680a072eee02f968055f3658bbfb7d386d0ea34cfb7f30/coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-27 12:28:51+00:00 +size = 267266 + +[packages.wheels.hashes] +sha256 = "dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/28/6c/f08e8ee4293e6434035424180bef4d45e028e8ecc006c61bf9453e74405e/coverage-7.16.2-cp315-cp315t-win32.whl" +upload-time = 2026-09-27 12:28:53+00:00 +size = 226583 + +[packages.wheels.hashes] +sha256 = "251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f7/fd/3f939c2847f4a72c20cff8b1ac33da78ea91a2d38d9b43336e60db719103/coverage-7.16.2-cp315-cp315t-win_amd64.whl" +upload-time = 2026-09-27 12:28:55+00:00 +size = 227374 + +[packages.wheels.hashes] +sha256 = "2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5a/35/b98cdc354c952402132e675a87f2cc3227fb68f959c84aaa491fbe15933d/coverage-7.16.2-cp315-cp315t-win_arm64.whl" +upload-time = 2026-09-27 12:28:57+00:00 +size = 227064 + +[packages.wheels.hashes] +sha256 = "b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/3f/0c/7a64e1ac90541a8edf50daef0914848011fb057a5bf55284a4811e21939a/coverage-7.16.2-py3-none-any.whl" +upload-time = 2026-09-27 12:28:59+00:00 +size = 215754 + +[packages.wheels.hashes] +sha256 = "11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f" + +[[packages]] +name = "cryptography" +version = "50.0.2" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz" +upload-time = 2026-09-30 15:30:04+00:00 +size = 880623 + +[packages.sdist.hashes] +sha256 = "7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl" +upload-time = 2026-09-30 14:43:44+00:00 +size = 3914904 + +[packages.wheels.hashes] +sha256 = "fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" +upload-time = 2026-09-30 14:43:47+00:00 +size = 4731146 + +[packages.wheels.hashes] +sha256 = "79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-09-30 14:43:49+00:00 +size = 4719841 + +[packages.wheels.hashes] +sha256 = "630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl" +upload-time = 2026-09-30 14:43:50+00:00 +size = 4738340 + +[packages.wheels.hashes] +sha256 = "f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl" +upload-time = 2026-09-30 14:43:52+00:00 +size = 5367029 + +[packages.wheels.hashes] +sha256 = "a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 14:43:55+00:00 +size = 4753050 + +[packages.wheels.hashes] +sha256 = "4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl" +upload-time = 2026-09-30 14:43:57+00:00 +size = 4376724 + +[packages.wheels.hashes] +sha256 = "ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl" +upload-time = 2026-09-30 14:43:59+00:00 +size = 4737859 + +[packages.wheels.hashes] +sha256 = "87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl" +upload-time = 2026-09-30 14:44:01+00:00 +size = 5324103 + +[packages.wheels.hashes] +sha256 = "f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl" +upload-time = 2026-09-30 14:44:04+00:00 +size = 4752576 + +[packages.wheels.hashes] +sha256 = "9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-30 14:44:06+00:00 +size = 4870819 + +[packages.wheels.hashes] +sha256 = "25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-30 14:44:09+00:00 +size = 5030152 + +[packages.wheels.hashes] +sha256 = "85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl" +upload-time = 2026-09-30 14:44:11+00:00 +size = 3824692 + +[packages.wheels.hashes] +sha256 = "7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl" +upload-time = 2026-09-30 14:44:41+00:00 +size = 4133708 + +[packages.wheels.hashes] +sha256 = "edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" +upload-time = 2026-09-30 14:44:43+00:00 +size = 4956267 + +[packages.wheels.hashes] +sha256 = "d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-09-30 14:44:45+00:00 +size = 4966465 + +[packages.wheels.hashes] +sha256 = "f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl" +upload-time = 2026-09-30 14:44:48+00:00 +size = 4959356 + +[packages.wheels.hashes] +sha256 = "e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl" +upload-time = 2026-09-30 14:44:50+00:00 +size = 5548822 + +[packages.wheels.hashes] +sha256 = "b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 14:44:53+00:00 +size = 5001199 + +[packages.wheels.hashes] +sha256 = "58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl" +upload-time = 2026-09-30 14:44:55+00:00 +size = 4629333 + +[packages.wheels.hashes] +sha256 = "d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl" +upload-time = 2026-09-30 14:44:59+00:00 +size = 4958822 + +[packages.wheels.hashes] +sha256 = "fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl" +upload-time = 2026-09-30 14:45:02+00:00 +size = 5506351 + +[packages.wheels.hashes] +sha256 = "cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl" +upload-time = 2026-09-30 14:45:05+00:00 +size = 5000859 + +[packages.wheels.hashes] +sha256 = "e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-30 15:29:15+00:00 +size = 5092151 + +[packages.wheels.hashes] +sha256 = "6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-30 15:29:18+00:00 +size = 5286120 + +[packages.wheels.hashes] +sha256 = "c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl" +upload-time = 2026-09-30 15:29:20+00:00 +size = 4111557 + +[packages.wheels.hashes] +sha256 = "c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl" +upload-time = 2026-09-30 15:29:22+00:00 +size = 3943588 + +[packages.wheels.hashes] +sha256 = "0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" +upload-time = 2026-09-30 15:29:24+00:00 +size = 4756166 + +[packages.wheels.hashes] +sha256 = "c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-09-30 15:29:26+00:00 +size = 4749145 + +[packages.wheels.hashes] +sha256 = "ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl" +upload-time = 2026-09-30 15:29:28+00:00 +size = 4763638 + +[packages.wheels.hashes] +sha256 = "dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl" +upload-time = 2026-09-30 15:29:30+00:00 +size = 5382217 + +[packages.wheels.hashes] +sha256 = "58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 15:29:32+00:00 +size = 4781387 + +[packages.wheels.hashes] +sha256 = "f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl" +upload-time = 2026-09-30 15:29:34+00:00 +size = 4403790 + +[packages.wheels.hashes] +sha256 = "9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl" +upload-time = 2026-09-30 15:29:36+00:00 +size = 4764319 + +[packages.wheels.hashes] +sha256 = "0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl" +upload-time = 2026-09-30 15:29:39+00:00 +size = 5338560 + +[packages.wheels.hashes] +sha256 = "a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl" +upload-time = 2026-09-30 15:29:41+00:00 +size = 4780973 + +[packages.wheels.hashes] +sha256 = "1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-30 15:29:43+00:00 +size = 4897738 + +[packages.wheels.hashes] +sha256 = "7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-30 15:29:44+00:00 +size = 5058280 + +[packages.wheels.hashes] +sha256 = "ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl" +upload-time = 2026-09-30 15:29:46+00:00 +size = 3854095 + +[packages.wheels.hashes] +sha256 = "4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de" + +[[packages]] +name = "cython" +version = "3.3.0" +marker = "\"build\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/a9/d8/4981ef716ad0e3ff0d3ef383aefc6b03c4a88dee33b272bf8e0d833001ca/cython-3.3.0.tar.gz" +upload-time = 2026-08-22 05:16:39+00:00 +size = 3727515 + +[packages.sdist.hashes] +sha256 = "eed0d93fbca7087f143b42c34b05a825849bdf17f101572c2105acfa49aa88b8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ea/45/7f6988070013e16918e39b1b3dab9c5f2c8e404253a7fd10ee685bbd6902/cython-3.3.0-cp315-cp315-macosx_11_0_arm64.whl" +upload-time = 2026-08-22 05:17:32+00:00 +size = 3177681 + +[packages.wheels.hashes] +sha256 = "596e8df019372a2cd417805015022d42cb8ee4e1803ccdc11ed00e451625fb66" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/6e/5d/afb6866ab10236bb208dff0f172ea4b397c9693c5250280c4d9d26057218/cython-3.3.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-08-22 05:17:34+00:00 +size = 3328312 + +[packages.wheels.hashes] +sha256 = "4a36c34d1950845b8ac148653b07cdc62421a4b0d9abfcc849e69f1c4ff9919d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c9/aa/4c0b6773ecf6bc1ec6cda7db8312a566611b330fb6dae87d740e44a47822/cython-3.3.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-08-22 05:17:36+00:00 +size = 3454285 + +[packages.wheels.hashes] +sha256 = "b447f6906e0555f05dc4742ef1f99091b1e5d9aa9f16616e772fbf9ff6271616" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/44/bb/3e2631122f96300723d6fc42b9cf65550bdcda570a6ad5c4e0226e2e787c/cython-3.3.0-cp315-cp315-win_amd64.whl" +upload-time = 2026-08-22 05:17:38+00:00 +size = 2924709 + +[packages.wheels.hashes] +sha256 = "b55c72e8eccdd508c8de3cf3bbc543aafbb3bf6a518e1ee20358d3241cd780ef" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/14/59/bc1a84b434cb5bebb0cd6f50da8f239d35a5c141b20fdeafc2817fd87778/cython-3.3.0-cp39-abi3-macosx_10_9_x86_64.whl" +upload-time = 2026-08-22 05:17:40+00:00 +size = 3063660 + +[packages.wheels.hashes] +sha256 = "e0d2713d2b292c826bc21dc8732bd9e47628103aa3764180c881e04b3fef95dc" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ba/6d/542e32908fb421d88354f327ed6450e14240f9825d25393065bc65f4723f/cython-3.3.0-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl" +upload-time = 2026-08-22 05:17:43+00:00 +size = 3358395 + +[packages.wheels.hashes] +sha256 = "169e56fd411f4cd5bba51c82f8239421d547a846099db2b261e4aed48ba9f51f" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/9c/7c/ddaf197bc65b581e1891657940bc4f7cb1f740e822115e828920b3a119ce/cython-3.3.0-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" +upload-time = 2026-08-22 05:17:44+00:00 +size = 3041760 + +[packages.wheels.hashes] +sha256 = "29f38ebafdf23e3da2516f40c4d065da38bfe002181bf93e2b8cf1262449aba6" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/19/a7/ae5ec3e34d43da846ed4c425734752d83aae0dae49feb929f09c90fc9afa/cython-3.3.0-cp39-abi3-musllinux_1_2_aarch64.whl" +upload-time = 2026-08-22 05:17:46+00:00 +size = 3156152 + +[packages.wheels.hashes] +sha256 = "75c4ae8a6d3a5ccf3cdaba8ab32e6a8d0cd38e3a476aa7ac12df8f8171a8d570" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/31/44/c60b601fc43f0b08e9d6f14b94e0dd02eb0ca8d60f46e242ace7191ac1be/cython-3.3.0-cp39-abi3-musllinux_1_2_armv7l.whl" +upload-time = 2026-08-22 05:17:48+00:00 +size = 3046690 + +[packages.wheels.hashes] +sha256 = "b94fb5613b9fe34c27d13ec9972dc0dcd2a2155db2902e93921cadc162610a38" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b0/9e/d735c26ed907563d3365534006acb263651c2d3b87fee804f7a483dd1714/cython-3.3.0-cp39-abi3-musllinux_1_2_i686.whl" +upload-time = 2026-08-22 05:17:50+00:00 +size = 3373534 + +[packages.wheels.hashes] +sha256 = "c4558ba85849ab65dc57e10fd0efb13fabd9d3c09981a2566e18dec7cf47586a" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/e0/e8/aa7b4f3a28d6e8117c76e2cf78a0df7a503486cdf7243c5b53200c9533a1/cython-3.3.0-cp39-abi3-musllinux_1_2_x86_64.whl" +upload-time = 2026-08-22 05:17:52+00:00 +size = 3265110 + +[packages.wheels.hashes] +sha256 = "311a016369adfd1e0015c4f9819168fc0e518451d7efb4435c30d65a3a26d52b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/9c/66/37892a8999d6bbd3f92d691a9701cb720c8ddd6171e16f5148eee6e8cb7f/cython-3.3.0-cp39-abi3-win32.whl" +upload-time = 2026-08-22 05:17:54+00:00 +size = 2587733 + +[packages.wheels.hashes] +sha256 = "90869072e50b7c8904fe1dd7810321ae901fd5637a6eec6646ed9c57f9eb1081" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/19/a2/5f4d305cbd4489d21570e5491ad5c483c478cdab032853e2125c280e3bd5/cython-3.3.0-cp39-abi3-win_arm64.whl" +upload-time = 2026-08-22 05:17:56+00:00 +size = 2608078 + +[packages.wheels.hashes] +sha256 = "dce56c26d388f00a19426371b6926bf2f77c5c03b71d5273e4556c68be98c2dd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/bf/77/67b0b24e45073a699610e50f00c18474ff9b09ea29ecc95083bdf5e60acd/cython-3.3.0-py3-none-any.whl" +upload-time = 2026-08-22 05:16:36+00:00 +size = 1349151 + +[packages.wheels.hashes] +sha256 = "9b24b5c8cd536946b62086fcafee6d5509d3f549f72d553d2336af87ffbe0da1" + +[[packages]] +name = "distlib" +version = "0.4.4" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz" +upload-time = 2026-10-16 13:56:34+00:00 +size = 621304 + +[packages.sdist.hashes] +sha256 = "ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl" +upload-time = 2026-10-16 13:56:32+00:00 +size = 472099 + +[packages.wheels.hashes] +sha256 = "cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638" + +[[packages]] +name = "dnspython" +version = "2.9.0" +marker = "\"release-attestations\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz" +upload-time = 2026-10-09 00:07:24+00:00 +size = 423560 + +[packages.sdist.hashes] +sha256 = "b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl" +upload-time = 2026-10-09 00:07:22+00:00 +size = 354822 + +[packages.wheels.hashes] +sha256 = "9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9" + +[[packages]] +name = "docutils" +version = "0.23" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz" +upload-time = 2026-05-27 17:41:06+00:00 +size = 2303823 + +[packages.sdist.hashes] +sha256 = "746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl" +upload-time = 2026-05-27 17:40:58+00:00 +size = 634701 + +[packages.wheels.hashes] +sha256 = "25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea" + +[[packages]] +name = "email-validator" +version = "2.3.0" +marker = "\"release-attestations\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz" +upload-time = 2025-08-26 13:09:06+00:00 +size = 51238 + +[packages.sdist.hashes] +sha256 = "9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl" +upload-time = 2025-08-26 13:09:05+00:00 +size = 35604 + +[packages.wheels.hashes] +sha256 = "80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4" + +[[packages]] +name = "execnet" +version = "2.1.2" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz" +upload-time = 2025-11-12 09:56:37+00:00 +size = 166622 + +[packages.sdist.hashes] +sha256 = "63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl" +upload-time = 2025-11-12 09:56:36+00:00 +size = 40708 + +[packages.wheels.hashes] +sha256 = "67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec" + +[[packages]] +name = "filelock" +version = "4.2.0" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/f4/a9/1af41b37c3279712b22cdc63aac78a52432202b6fe1f9666a2a3d2831fb4/filelock-4.2.0.tar.gz" +upload-time = 2026-10-14 20:57:13+00:00 +size = 569667 + +[packages.sdist.hashes] +sha256 = "7a60906c75227cf04d0c273afadc8219400f11aeb13cc69591d4f6cdc6c8036e" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8e/a3/9bc26acff301fe1aaea1cc3d82a1d57e0a34df3e1cadbfa91ac2dbcdde5c/filelock-4.2.0-py3-none-any.whl" +upload-time = 2026-10-14 20:57:11+00:00 +size = 135032 + +[packages.wheels.hashes] +sha256 = "2ff5690882e8cdb00ef31fb3d01a3094c29f30985426c59495afb1733f3b7238" + +[[packages]] +name = "hatchling" +version = "1.32.4" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/f6/97/b5312f01a8c6daf729a9d272dd442e0c546dbcc630495788786c4b567ed0/hatchling-1.32.4.tar.gz" +upload-time = 2026-09-20 22:48:45+00:00 +size = 58065 + +[packages.sdist.hashes] +sha256 = "c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5f/80/91f51f439c05d4ec4623c22928ce16a938d6d793bf709477830823497859/hatchling-1.32.4-py3-none-any.whl" +upload-time = 2026-09-20 22:48:44+00:00 +size = 78748 + +[packages.wheels.hashes] +sha256 = "08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c" + +[[packages]] +name = "id" +version = "1.6.1" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz" +upload-time = 2026-02-04 16:19:41+00:00 +size = 18088 + +[packages.sdist.hashes] +sha256 = "d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl" +upload-time = 2026-02-04 16:19:40+00:00 +size = 14689 + +[packages.wheels.hashes] +sha256 = "f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca" + +[[packages]] +name = "idna" +version = "3.20" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz" +upload-time = 2026-09-17 14:11:04+00:00 +size = 216463 + +[packages.sdist.hashes] +sha256 = "a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl" +upload-time = 2026-09-17 14:11:03+00:00 +size = 69583 + +[packages.wheels.hashes] +sha256 = "ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c" + +[[packages]] +name = "iniconfig" +version = "2.3.1" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz" +upload-time = 2026-10-06 22:48:38+00:00 +size = 21209 + +[packages.sdist.hashes] +sha256 = "67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl" +upload-time = 2026-10-06 22:48:36+00:00 +size = 7552 + +[packages.wheels.hashes] +sha256 = "9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7" + +[[packages]] +name = "installer" +version = "1.1.0" +marker = "\"lock-tools\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/56/d3/2fab33d15004d41ae8e7b5d3e310510f7f34a7e1dd032a3e60d8b38d8fba/installer-1.1.0.tar.gz" +upload-time = 2026-10-12 19:35:52+00:00 +size = 481257 + +[packages.sdist.hashes] +sha256 = "5adc0c01ce62d809989e246987cd68944fe5df8af562d0358f65b2ab15cb5de7" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d7/7a/63547a792a12148882205d79e07644091d5b7690f40c2bb9d41452a8bdc2/installer-1.1.0-py3-none-any.whl" +upload-time = 2026-10-12 19:35:51+00:00 +size = 464466 + +[packages.wheels.hashes] +sha256 = "f55f5728ff61d3dd54dbc5a397cf3f7307cb64b180c20b72afbebf01c9e2c8a0" + +[[packages]] +name = "jaraco-classes" +version = "3.4.0" +marker = "(\"test\" in dependency_groups and (platform_machine != 'ppc64le' and platform_machine != 's390x'))" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" +upload-time = 2024-03-31 07:27:36+00:00 +size = 11780 + +[packages.sdist.hashes] +sha256 = "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl" +upload-time = 2024-03-31 07:27:34+00:00 +size = 6777 + +[packages.wheels.hashes] +sha256 = "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + +[[packages]] +name = "jaraco-context" +version = "6.1.2" +marker = "(\"test\" in dependency_groups and (platform_machine != 'ppc64le' and platform_machine != 's390x'))" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz" +upload-time = 2026-03-20 22:13:33+00:00 +size = 16801 + +[packages.sdist.hashes] +sha256 = "f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl" +upload-time = 2026-03-20 22:13:32+00:00 +size = 7871 + +[packages.wheels.hashes] +sha256 = "bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535" + +[[packages]] +name = "jaraco-functools" +version = "4.6.0" +marker = "(\"test\" in dependency_groups and (platform_machine != 'ppc64le' and platform_machine != 's390x'))" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz" +upload-time = 2026-07-14 01:28:02+00:00 +size = 20837 + +[packages.sdist.hashes] +sha256 = "880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl" +upload-time = 2026-07-14 01:28:01+00:00 +size = 11677 + +[packages.wheels.hashes] +sha256 = "99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30" + +[[packages]] +name = "jeepney" +version = "0.9.0" +marker = "(\"test\" in dependency_groups and (platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux'))" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz" +upload-time = 2025-02-27 18:51:01+00:00 +size = 106758 + +[packages.sdist.hashes] +sha256 = "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl" +upload-time = 2025-02-27 18:51:00+00:00 +size = 49010 + +[packages.wheels.hashes] +sha256 = "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" + +[[packages]] +name = "jinja2" +version = "3.1.6" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz" +upload-time = 2025-03-05 20:05:02+00:00 +size = 245115 + +[packages.sdist.hashes] +sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl" +upload-time = 2025-03-05 20:05:00+00:00 +size = 134899 + +[packages.wheels.hashes] +sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" + +[[packages]] +name = "keyring" +version = "25.7.0" +marker = "(\"test\" in dependency_groups and (platform_machine != 'ppc64le' and platform_machine != 's390x'))" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz" +upload-time = 2025-11-16 16:26:09+00:00 +size = 63516 + +[packages.sdist.hashes] +sha256 = "fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl" +upload-time = 2025-11-16 16:26:08+00:00 +size = 39160 + +[packages.wheels.hashes] +sha256 = "be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" + +[[packages]] +name = "markdown-it-py" +version = "4.2.0" +marker = "\"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz" +upload-time = 2026-05-07 12:08:28+00:00 +size = 82454 + +[packages.sdist.hashes] +sha256 = "04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl" +upload-time = 2026-05-07 12:08:27+00:00 +size = 91687 + +[packages.wheels.hashes] +sha256 = "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" + +[[packages]] +name = "markupsafe" +version = "3.0.4" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz" +upload-time = 2026-10-02 23:07:22+00:00 +size = 153777 + +[packages.sdist.hashes] +sha256 = "2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl" +upload-time = 2026-10-02 23:06:10+00:00 +size = 12791 + +[packages.wheels.hashes] +sha256 = "737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl" +upload-time = 2026-10-02 23:06:11+00:00 +size = 12527 + +[packages.wheels.hashes] +sha256 = "489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl" +upload-time = 2026-10-02 23:06:13+00:00 +size = 11602 + +[packages.wheels.hashes] +sha256 = "077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl" +upload-time = 2026-10-02 23:06:14+00:00 +size = 11817 + +[packages.wheels.hashes] +sha256 = "9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl" +upload-time = 2026-10-02 23:06:15+00:00 +size = 11446 + +[packages.wheels.hashes] +sha256 = "b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl" +upload-time = 2026-10-02 23:06:16+00:00 +size = 11719 + +[packages.wheels.hashes] +sha256 = "799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl" +upload-time = 2026-10-02 23:06:17+00:00 +size = 12075 + +[packages.wheels.hashes] +sha256 = "ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-10-02 23:06:18+00:00 +size = 24656 + +[packages.wheels.hashes] +sha256 = "4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" +upload-time = 2026-10-02 23:06:20+00:00 +size = 27737 + +[packages.wheels.hashes] +sha256 = "3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" +upload-time = 2026-10-02 23:06:21+00:00 +size = 25003 + +[packages.wheels.hashes] +sha256 = "04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-10-02 23:06:22+00:00 +size = 23111 + +[packages.wheels.hashes] +sha256 = "925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" +upload-time = 2026-10-02 23:06:23+00:00 +size = 22403 + +[packages.wheels.hashes] +sha256 = "f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl" +upload-time = 2026-10-02 23:06:25+00:00 +size = 24083 + +[packages.wheels.hashes] +sha256 = "e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl" +upload-time = 2026-10-02 23:06:26+00:00 +size = 22931 + +[packages.wheels.hashes] +sha256 = "55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl" +upload-time = 2026-10-02 23:06:27+00:00 +size = 24299 + +[packages.wheels.hashes] +sha256 = "2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl" +upload-time = 2026-10-02 23:06:29+00:00 +size = 21948 + +[packages.wheels.hashes] +sha256 = "94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl" +upload-time = 2026-10-02 23:06:30+00:00 +size = 23150 + +[packages.wheels.hashes] +sha256 = "2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl" +upload-time = 2026-10-02 23:06:31+00:00 +size = 14269 + +[packages.wheels.hashes] +sha256 = "03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl" +upload-time = 2026-10-02 23:06:33+00:00 +size = 14496 + +[packages.wheels.hashes] +sha256 = "d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl" +upload-time = 2026-10-02 23:06:34+00:00 +size = 14361 + +[packages.wheels.hashes] +sha256 = "353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl" +upload-time = 2026-10-02 23:06:35+00:00 +size = 11760 + +[packages.wheels.hashes] +sha256 = "c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl" +upload-time = 2026-10-02 23:06:37+00:00 +size = 12108 + +[packages.wheels.hashes] +sha256 = "1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-10-02 23:06:38+00:00 +size = 25762 + +[packages.wheels.hashes] +sha256 = "02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" +upload-time = 2026-10-02 23:06:39+00:00 +size = 28400 + +[packages.wheels.hashes] +sha256 = "05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" +upload-time = 2026-10-02 23:06:41+00:00 +size = 25946 + +[packages.wheels.hashes] +sha256 = "be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-10-02 23:06:42+00:00 +size = 23634 + +[packages.wheels.hashes] +sha256 = "26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" +upload-time = 2026-10-02 23:06:44+00:00 +size = 23488 + +[packages.wheels.hashes] +sha256 = "f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl" +upload-time = 2026-10-02 23:06:45+00:00 +size = 25037 + +[packages.wheels.hashes] +sha256 = "436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl" +upload-time = 2026-10-02 23:06:47+00:00 +size = 23543 + +[packages.wheels.hashes] +sha256 = "4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl" +upload-time = 2026-10-02 23:06:48+00:00 +size = 25282 + +[packages.wheels.hashes] +sha256 = "da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl" +upload-time = 2026-10-02 23:06:50+00:00 +size = 22993 + +[packages.wheels.hashes] +sha256 = "aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl" +upload-time = 2026-10-02 23:06:51+00:00 +size = 23712 + +[packages.wheels.hashes] +sha256 = "b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl" +upload-time = 2026-10-02 23:06:53+00:00 +size = 14300 + +[packages.wheels.hashes] +sha256 = "e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl" +upload-time = 2026-10-02 23:06:54+00:00 +size = 14522 + +[packages.wheels.hashes] +sha256 = "4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl" +upload-time = 2026-10-02 23:06:56+00:00 +size = 14417 + +[packages.wheels.hashes] +sha256 = "39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba" + +[[packages]] +name = "mdformat" +version = "1.0.0" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz" +upload-time = 2025-10-16 12:05:03+00:00 +size = 56953 + +[packages.sdist.hashes] +sha256 = "4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl" +upload-time = 2025-10-16 12:05:02+00:00 +size = 53288 + +[packages.wheels.hashes] +sha256 = "bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b" + +[[packages]] +name = "mdformat-gfm" +version = "1.0.0" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz" +upload-time = 2025-10-16 09:12:22+00:00 +size = 10112 + +[packages.sdist.hashes] +sha256 = "d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl" +upload-time = 2025-10-16 09:12:21+00:00 +size = 10970 + +[packages.wheels.hashes] +sha256 = "7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005" + +[[packages]] +name = "mdit-py-plugins" +version = "0.6.1" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz" +upload-time = 2026-05-13 09:03:38+00:00 +size = 56114 + +[packages.sdist.hashes] +sha256 = "a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl" +upload-time = 2026-05-13 09:03:37+00:00 +size = 66663 + +[packages.wheels.hashes] +sha256 = "214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d" + +[[packages]] +name = "mdurl" +version = "0.1.2" +marker = "\"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" +upload-time = 2022-08-14 12:40:10+00:00 +size = 8729 + +[packages.sdist.hashes] +sha256 = "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" +upload-time = 2022-08-14 12:40:09+00:00 +size = 9979 + +[packages.wheels.hashes] +sha256 = "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" + +[[packages]] +name = "more-itertools" +version = "11.2.1" +marker = "(\"test\" in dependency_groups and (platform_machine != 'ppc64le' and platform_machine != 's390x'))" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/52/b8/c63995bebbb9ac9c059350e0fb8c82c2420c85aeca7d34e40aecf3df9281/more_itertools-11.2.1.tar.gz" +upload-time = 2026-10-14 18:50:30+00:00 +size = 154133 + +[packages.sdist.hashes] +sha256 = "cbf08fd0af284dc69718b9b76a8e6203df624d70209ea511e4219d350c856f63" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5d/57/3c4b1c6f2e0cc29ba023802458ea3d2a996f87085559e4a5f2192d549532/more_itertools-11.2.1-py3-none-any.whl" +upload-time = 2026-10-14 18:50:29+00:00 +size = 74405 + +[packages.wheels.hashes] +sha256 = "35a7377edd1dd6608dcb2cdf534ded55ea32d49448875ad042cd3f879fb1ded0" + +[[packages]] +name = "nab" +version = "0.0.22" +marker = "\"lock-tools\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/25/3d/e4c8f4b2890efbf2fa6eef82acf57ecc0ef39b0a1e6843d306d2053a107c/nab-0.0.22.tar.gz" +upload-time = 2026-10-18 05:45:08+00:00 +size = 286128 + +[packages.sdist.hashes] +sha256 = "a0da54ee96bf1eb1997581566620c250c8a3557139e9467ccaced2a12e9e71ac" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/56/4b/77baa0ddb0b7578b6475b2b578fa4579207b0f97e0b3b80480e01dcb3d6f/nab-0.0.22-py3-none-any.whl" +upload-time = 2026-10-18 05:45:06+00:00 +size = 137802 + +[packages.wheels.hashes] +sha256 = "f62fcf75b28786fbf58627f6d7eaa735daefbc7f736715403bc4357acb57e7bb" + +[[packages]] +name = "nab-index" +version = "0.0.22" +marker = "\"lock-tools\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/2a/83/8d2483239b7dc5ac8d9f758db1cc19d4129d0143d34c2e1ab8cd48ea71fb/nab_index-0.0.22.tar.gz" +upload-time = 2026-10-18 05:44:43+00:00 +size = 110621 + +[packages.sdist.hashes] +sha256 = "5900ea5ad298512379cfddff1a73a544590f0b04620ef58ff345f99a7c59688e" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c0/58/c06ada29934a78479b989675829f26083972f38bf1e8a2503e01101cb970/nab_index-0.0.22-py3-none-any.whl" +upload-time = 2026-10-18 05:44:42+00:00 +size = 88373 + +[packages.wheels.hashes] +sha256 = "c89ffa607dfe4246dc59e9f5236d54413da3d310183ffa3298ba51a48e7c2ec7" + +[[packages]] +name = "nab-markersets" +version = "0.0.22" +marker = "\"lock-tools\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/fc/b7/3997893dfdada9998d4dc786c10445a1361e197b4aed54ddd866c84dd538/nab_markersets-0.0.22.tar.gz" +upload-time = 2026-10-18 05:44:19+00:00 +size = 109258 + +[packages.sdist.hashes] +sha256 = "38212664f517f75d798808ecfe902bfd11fef340042ab0203100c7e06e78eea5" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5e/9e/9744adff3a66633a811f8253faaa8b91a447c99199edba16f2790e3647ea/nab_markersets-0.0.22-py3-none-any.whl" +upload-time = 2026-10-18 05:44:18+00:00 +size = 44360 + +[packages.wheels.hashes] +sha256 = "ad268dcece034495b2ea31a70250cd43cc4f6123039a548762c7ec8826e5d3bd" + +[[packages]] +name = "nab-project" +version = "0.0.22" +marker = "\"lock-tools\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/65/84/f5197d4044ceda9c6ee3e7aefce154ab616fec632fa1d89cdfaacc665c07/nab_project-0.0.22.tar.gz" +upload-time = 2026-10-18 05:44:55+00:00 +size = 774409 + +[packages.sdist.hashes] +sha256 = "5f7a1dee5b618149267b2b6233c3a220e45cd6f97bd5ec7f3f48fa7570e9f053" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/de/c9/62affa4df6cabce48751307c4c3adfbaa8c54e402d464d015897a16a6dba/nab_project-0.0.22-py3-none-any.whl" +upload-time = 2026-10-18 05:44:53+00:00 +size = 148262 + +[packages.wheels.hashes] +sha256 = "cb09fa143105fd99a60609b3ff4830924b0d4eaf5abad48678467ec4b73e577f" + +[[packages]] +name = "nab-provider" +version = "0.0.22" +marker = "\"lock-tools\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/94/0d/64fbadae562fefc00257c4f5ec988171d6e6b3ea4363f1da2e8886e80228/nab_provider-0.0.22.tar.gz" +upload-time = 2026-10-18 05:44:31+00:00 +size = 409367 + +[packages.sdist.hashes] +sha256 = "3f129ab7394bf5eb031152c3535ee74073e2a6b6ae081fb7f5a4d269fcf5f8a0" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/30/67/32bda18d34f0f8ec098f8e4a0f792164ef1a4b2aa61d435d20f4645572af/nab_provider-0.0.22-py3-none-any.whl" +upload-time = 2026-10-18 05:44:29+00:00 +size = 339242 + +[packages.wheels.hashes] +sha256 = "1450c9dc58b23da1a7bde7fdb43a72b043327bd0e3ef20435254ffcb8b7eae6b" + +[[packages]] +name = "nab-resolver" +version = "0.0.22" +marker = "\"lock-tools\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/84/bc/dcbfe5163538b1cc8421797e3f8ff77d79321cbc9943af8065e2c8ae1c09/nab_resolver-0.0.22.tar.gz" +upload-time = 2026-10-18 05:44:08+00:00 +size = 167648 + +[packages.sdist.hashes] +sha256 = "e005b225213e032858cf4aef3850961aaa1d4ea521428da731f33175ebd3b05f" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/22/a8/6e30b25ae12e0c662dc9b181a7f32fa8cd22f26016f12dc1d10176f62bba/nab_resolver-0.0.22-py3-none-any.whl" +upload-time = 2026-10-18 05:44:06+00:00 +size = 58597 + +[packages.wheels.hashes] +sha256 = "6f77daa98279ef0eedb9e8f2248aee4c50e01b92b470b68886995e5056d79971" + +[[packages]] +name = "nh3" +version = "0.3.7" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz" +upload-time = 2026-08-23 14:26:30+00:00 +size = 25662 + +[packages.sdist.hashes] +sha256 = "71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" +upload-time = 2026-08-23 14:26:09+00:00 +size = 1493959 + +[packages.wheels.hashes] +sha256 = "6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" +upload-time = 2026-08-23 14:26:10+00:00 +size = 859615 + +[packages.wheels.hashes] +sha256 = "f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" +upload-time = 2026-08-23 14:26:12+00:00 +size = 839872 + +[packages.wheels.hashes] +sha256 = "e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl" +upload-time = 2026-08-23 14:26:13+00:00 +size = 1091325 + +[packages.wheels.hashes] +sha256 = "19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" +upload-time = 2026-08-23 14:26:14+00:00 +size = 1042482 + +[packages.wheels.hashes] +sha256 = "de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl" +upload-time = 2026-08-23 14:26:16+00:00 +size = 946868 + +[packages.wheels.hashes] +sha256 = "9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-08-23 14:26:17+00:00 +size = 832161 + +[packages.wheels.hashes] +sha256 = "70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl" +upload-time = 2026-08-23 14:26:18+00:00 +size = 849791 + +[packages.wheels.hashes] +sha256 = "5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl" +upload-time = 2026-08-23 14:26:19+00:00 +size = 875473 + +[packages.wheels.hashes] +sha256 = "bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl" +upload-time = 2026-08-23 14:26:21+00:00 +size = 1036463 + +[packages.wheels.hashes] +sha256 = "d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl" +upload-time = 2026-08-23 14:26:22+00:00 +size = 1116029 + +[packages.wheels.hashes] +sha256 = "fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl" +upload-time = 2026-08-23 14:26:24+00:00 +size = 1076589 + +[packages.wheels.hashes] +sha256 = "15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl" +upload-time = 2026-08-23 14:26:25+00:00 +size = 1058871 + +[packages.wheels.hashes] +sha256 = "6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/4b/6a/478f153f1d7c0baaa3d1e8bb5fdcee3a6235f90fe44ea969a9d4e2b8c47a/nh3-0.3.7-cp38-abi3-win32.whl" +upload-time = 2026-08-23 14:26:26+00:00 +size = 630729 + +[packages.wheels.hashes] +sha256 = "6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl" +upload-time = 2026-08-23 14:26:28+00:00 +size = 644462 + +[packages.wheels.hashes] +sha256 = "618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl" +upload-time = 2026-08-23 14:26:29+00:00 +size = 621867 + +[packages.wheels.hashes] +sha256 = "f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a" + +[[packages]] +name = "packaging" +version = "26.3" +marker = "\"lock-reader\" in dependency_groups or \"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz" +upload-time = 2026-08-04 18:15:28+00:00 +size = 313412 + +[packages.sdist.hashes] +sha256 = "94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl" +upload-time = 2026-08-04 18:15:27+00:00 +size = 129956 + +[packages.wheels.hashes] +sha256 = "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" + +[[packages]] +name = "pathspec" +version = "1.1.1" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz" +upload-time = 2026-04-27 01:46:08+00:00 +size = 135180 + +[packages.sdist.hashes] +sha256 = "17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl" +upload-time = 2026-04-27 01:46:07+00:00 +size = 57328 + +[packages.wheels.hashes] +sha256 = "a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" + +[[packages]] +name = "platformdirs" +version = "4.13.3" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/da/91/4a03cfdb03314cfca262921797fff04ff4054bd86a160aed76eddf8aa12b/platformdirs-4.13.3.tar.gz" +upload-time = 2026-10-16 01:16:16+00:00 +size = 61559 + +[packages.sdist.hashes] +sha256 = "5e567f664eb087ab8521c0179cd8d1bd60857d271136567a39719e28e2d383ce" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/45/b8/fd1af06b079af236f5423f7c1821264419cc8f6b4803f79353acbb8bfa53/platformdirs-4.13.3-py3-none-any.whl" +upload-time = 2026-10-16 01:16:15+00:00 +size = 32762 + +[packages.wheels.hashes] +sha256 = "f6ad7f447f24f8a3b82cce5976387428bff894a0eca6c3488f4a17f153c130c4" + +[[packages]] +name = "pluggy" +version = "1.7.0" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz" +upload-time = 2026-10-15 09:50:58+00:00 +size = 123304 + +[packages.sdist.hashes] +sha256 = "d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl" +upload-time = 2026-10-15 09:50:56+00:00 +size = 27082 + +[packages.wheels.hashes] +sha256 = "7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec" + +[[packages]] +name = "prek" +version = "0.5.5" +marker = "\"release-check\" in dependency_groups or \"release-hook-check\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-shell\" in dependency_groups or \"release-validate\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/9b/7b/11538b8e6cd7e269261feae62a2d08818fdcf8f43c3b1a253cec01a5488d/prek-0.5.5.tar.gz" +upload-time = 2026-10-05 06:08:59+00:00 +size = 570428 + +[packages.sdist.hashes] +sha256 = "16bf596768bb754779c7d42317e9ca16ac609c0cce7d13bae0829944ab40a26a" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/2e/77/189e554ea92baf6c1d33733b3e9d1ed819ab7d77e57eb2e3ead0bb1d8060/prek-0.5.5-py3-none-macosx_10_12_x86_64.whl" +upload-time = 2026-10-05 06:08:39+00:00 +size = 6038669 + +[packages.wheels.hashes] +sha256 = "50fbceb0f728b504023f6e6c9037ac0656436949d94f7a85bac5542e6c1a4a75" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/63/ea/fc857c6b3f1e3fdb4d81bdca2051076de82e986cb1455a01b351ac7a5cd1/prek-0.5.5-py3-none-macosx_11_0_arm64.whl" +upload-time = 2026-10-05 06:08:41+00:00 +size = 5548955 + +[packages.wheels.hashes] +sha256 = "2f610927b95e84b28173a4c810892a7b5d5bb55bee792ddf97178fa97654633e" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/47/c3/2bab54f6393fb9f247225681d476086e24fa6242f329e9ca1e5bd638ec50/prek-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl" +upload-time = 2026-10-05 06:08:44+00:00 +size = 5848228 + +[packages.wheels.hashes] +sha256 = "120ef7d4b6bb58155e74827743c266e48501efb0e4c37ff726b14fe6e86a126b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/aa/f9/1279e1c83819de5904db1a09f4afc589932a2e62c7063713b1fe3a54ee46/prek-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" +upload-time = 2026-10-05 06:08:46+00:00 +size = 6371296 + +[packages.wheels.hashes] +sha256 = "2f6c453a12c21757b23bbbc97415aef78527841ad3f9e61f52cb35cf012f93e5" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/93/fc/e566be5919eaffef4cc22488f16e4bbfd08961734bd67c06f85d405e4cbc/prek-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-05 06:08:48+00:00 +size = 6269922 + +[packages.wheels.hashes] +sha256 = "73d509c0d9556171de2502bada4962be7a25871e7b23e782549074e1416dc850" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d2/39/a48d8e6203055bff7bb734b1f2f9aede4a4539bee6ab4c9273dbc01fa546/prek-0.5.5-py3-none-manylinux_2_28_aarch64.whl" +upload-time = 2026-10-05 06:08:50+00:00 +size = 5861518 + +[packages.wheels.hashes] +sha256 = "76c867fdbd4e4f19e6b6b2988bd1e1407d1324170ebcf1428e4e34f8f0751a99" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b7/03/690fba9543033e25ea2e7b2ae1cca1ae8d8c2e32295f0d66f4b50934b8c7/prek-0.5.5-py3-none-musllinux_1_1_x86_64.whl" +upload-time = 2026-10-05 06:08:52+00:00 +size = 6383685 + +[packages.wheels.hashes] +sha256 = "c69f60c2b3b4d481e38d0eab3b97ac05ec05d7648ae690925ae95ca9d22c7643" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8c/04/1e9ccf3a61a3bfd4107b83973f7afde9177b6ff8f2d5d4b00125e77c2100/prek-0.5.5-py3-none-win_amd64.whl" +upload-time = 2026-10-05 06:08:55+00:00 +size = 5784759 + +[packages.wheels.hashes] +sha256 = "15bff9039349466ea9bf6b0c173b822ff08f8ae4cfbfdea782492e519c06d874" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/41/d9/bb6811e438d3c74d26cb5d72e6343606e0a7a6283fee87521189f4ba1191/prek-0.5.5-py3-none-win_arm64.whl" +upload-time = 2026-10-05 06:08:57+00:00 +size = 5517355 + +[packages.wheels.hashes] +sha256 = "3db3191d2509830c75438e20956a47c2259867d18a4106064ed75088b968a0c6" + +[[packages]] +name = "pyasn1" +version = "0.6.4" +marker = "\"release-attestations\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz" +upload-time = 2026-07-09 01:12:33+00:00 +size = 151262 + +[packages.sdist.hashes] +sha256 = "9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl" +upload-time = 2026-07-09 01:12:32+00:00 +size = 84410 + +[packages.wheels.hashes] +sha256 = "deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b" + +[[packages]] +name = "pycparser" +version = "3.11" +marker = "(\"release-attestations\" in dependency_groups and (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')) or (\"test\" in dependency_groups and (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'))" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz" +upload-time = 2026-10-09 12:56:59+00:00 +size = 113796 + +[packages.sdist.hashes] +sha256 = "d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl" +upload-time = 2026-10-09 12:56:58+00:00 +size = 51178 + +[packages.wheels.hashes] +sha256 = "51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80" + +[[packages]] +name = "pydantic" +version = "2.14.1" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz" +upload-time = 2026-10-11 18:37:55+00:00 +size = 900362 + +[packages.sdist.hashes] +sha256 = "94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl" +upload-time = 2026-10-11 18:37:53+00:00 +size = 488106 + +[packages.wheels.hashes] +sha256 = "9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454" + +[[packages]] +name = "pydantic-core" +version = "2.50.1" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz" +upload-time = 2026-10-11 18:35:44+00:00 +size = 513515 + +[packages.sdist.hashes] +sha256 = "e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl" +upload-time = 2026-10-11 18:34:04+00:00 +size = 2224599 + +[packages.wheels.hashes] +sha256 = "8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl" +upload-time = 2026-10-11 18:34:05+00:00 +size = 2024191 + +[packages.wheels.hashes] +sha256 = "84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" +upload-time = 2026-10-11 18:34:07+00:00 +size = 2048252 + +[packages.wheels.hashes] +sha256 = "23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" +upload-time = 2026-10-11 18:34:09+00:00 +size = 2173376 + +[packages.wheels.hashes] +sha256 = "3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" +upload-time = 2026-10-11 18:34:11+00:00 +size = 2364559 + +[packages.wheels.hashes] +sha256 = "c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl" +upload-time = 2026-10-11 18:34:13+00:00 +size = 2444697 + +[packages.wheels.hashes] +sha256 = "ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-11 18:34:15+00:00 +size = 2159961 + +[packages.wheels.hashes] +sha256 = "7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl" +upload-time = 2026-10-11 18:34:17+00:00 +size = 2234581 + +[packages.wheels.hashes] +sha256 = "d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl" +upload-time = 2026-10-11 18:34:19+00:00 +size = 2244090 + +[packages.wheels.hashes] +sha256 = "40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl" +upload-time = 2026-10-11 18:34:21+00:00 +size = 2277948 + +[packages.wheels.hashes] +sha256 = "d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl" +upload-time = 2026-10-11 18:34:23+00:00 +size = 2449688 + +[packages.wheels.hashes] +sha256 = "84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl" +upload-time = 2026-10-11 18:34:25+00:00 +size = 2479286 + +[packages.wheels.hashes] +sha256 = "ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl" +upload-time = 2026-10-11 18:34:27+00:00 +size = 2103683 + +[packages.wheels.hashes] +sha256 = "bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl" +upload-time = 2026-10-11 18:34:29+00:00 +size = 2171681 + +[packages.wheels.hashes] +sha256 = "983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl" +upload-time = 2026-10-11 18:34:31+00:00 +size = 2156251 + +[packages.wheels.hashes] +sha256 = "94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl" +upload-time = 2026-10-11 18:34:33+00:00 +size = 2234363 + +[packages.wheels.hashes] +sha256 = "4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl" +upload-time = 2026-10-11 18:34:36+00:00 +size = 2000972 + +[packages.wheels.hashes] +sha256 = "efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" +upload-time = 2026-10-11 18:34:37+00:00 +size = 2022925 + +[packages.wheels.hashes] +sha256 = "bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" +upload-time = 2026-10-11 18:34:39+00:00 +size = 2192867 + +[packages.wheels.hashes] +sha256 = "5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" +upload-time = 2026-10-11 18:34:42+00:00 +size = 2388687 + +[packages.wheels.hashes] +sha256 = "adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl" +upload-time = 2026-10-11 18:34:44+00:00 +size = 2464415 + +[packages.wheels.hashes] +sha256 = "2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-11 18:34:46+00:00 +size = 2150823 + +[packages.wheels.hashes] +sha256 = "23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl" +upload-time = 2026-10-11 18:34:48+00:00 +size = 2245806 + +[packages.wheels.hashes] +sha256 = "f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl" +upload-time = 2026-10-11 18:34:50+00:00 +size = 2264160 + +[packages.wheels.hashes] +sha256 = "455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl" +upload-time = 2026-10-11 18:34:52+00:00 +size = 2294031 + +[packages.wheels.hashes] +sha256 = "1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl" +upload-time = 2026-10-11 18:34:54+00:00 +size = 2468423 + +[packages.wheels.hashes] +sha256 = "d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl" +upload-time = 2026-10-11 18:34:57+00:00 +size = 2512598 + +[packages.wheels.hashes] +sha256 = "009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl" +upload-time = 2026-10-11 18:34:59+00:00 +size = 2098535 + +[packages.wheels.hashes] +sha256 = "3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl" +upload-time = 2026-10-11 18:35:01+00:00 +size = 2148645 + +[packages.wheels.hashes] +sha256 = "1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl" +upload-time = 2026-10-11 18:35:04+00:00 +size = 2175297 + +[packages.wheels.hashes] +sha256 = "c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44" + +[[packages]] +name = "pygments" +version = "2.21.0" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz" +upload-time = 2026-08-17 08:02:48+00:00 +size = 5005329 + +[packages.sdist.hashes] +sha256 = "610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl" +upload-time = 2026-08-17 08:02:44+00:00 +size = 1250147 + +[packages.wheels.hashes] +sha256 = "2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9" + +[[packages]] +name = "pyjwt" +version = "2.15.1" +marker = "\"release-attestations\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz" +upload-time = 2026-09-28 18:40:42+00:00 +size = 121252 + +[packages.sdist.hashes] +sha256 = "4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl" +upload-time = 2026-09-28 18:40:41+00:00 +size = 33860 + +[packages.wheels.hashes] +sha256 = "42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193" + +[[packages]] +name = "pyopenssl" +version = "26.4.0" +marker = "\"release-attestations\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz" +upload-time = 2026-08-01 19:50:50+00:00 +size = 182046 + +[packages.sdist.hashes] +sha256 = "28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl" +upload-time = 2026-08-01 19:50:48+00:00 +size = 56026 + +[packages.wheels.hashes] +sha256 = "f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c" + +[[packages]] +name = "pypi-attestations" +version = "0.0.30" +marker = "\"release-attestations\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/97/1e/eb8a0233cdcaf01acb18076ac8b8a057714177b3717baf26b78a11b8d929/pypi_attestations-0.0.30.tar.gz" +upload-time = 2026-07-28 14:08:35+00:00 +size = 139108 + +[packages.sdist.hashes] +sha256 = "14ff13c76bbef02a483c2b77532da02bfd746b5141b881f355e06b9807434423" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/5f/24/e59078318b5e2bca59be5a21957d70e082e6eb4f478adffe373f3b74daf4/pypi_attestations-0.0.30-py3-none-any.whl" +upload-time = 2026-07-28 14:08:33+00:00 +size = 23015 + +[packages.wheels.hashes] +sha256 = "b3a9c53f6cb89e5e7b5b70e6cfca97cfc66008c1ed54087355e06e40071cef21" + +[[packages]] +name = "pyproject-api" +version = "1.11.7" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/ce/04/15a06959a6a40d62b5aa001f6df5d26bba3961a84563f745ac337a79e979/pyproject_api-1.11.7.tar.gz" +upload-time = 2026-10-15 21:59:41+00:00 +size = 26054 + +[packages.sdist.hashes] +sha256 = "408e31b80e054e70d26f11b0ae3f60210ae6d54cd050e904aac63462b56404e9" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c0/ab/ed567ad9253b3528e8b16d124944ca646cc8737578c6aa0838b21155e461/pyproject_api-1.11.7-py3-none-any.whl" +upload-time = 2026-10-15 21:59:40+00:00 +size = 13594 + +[packages.wheels.hashes] +sha256 = "e90ddef94bd3027c652e11b04537d269788b6c5d75311c139ffb372e4fcb562c" + +[[packages]] +name = "pyproject-hooks" +version = "1.3.3" +marker = "\"lock-tools\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/6d/5d/f2ddeef4a855a102aaae5e97826a0260007522ab504421b75addfdb1517c/pyproject_hooks-1.3.3.tar.gz" +upload-time = 2026-09-16 08:58:03+00:00 +size = 21013 + +[packages.sdist.hashes] +sha256 = "defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/85/11/044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977/pyproject_hooks-1.3.3-py3-none-any.whl" +upload-time = 2026-09-16 08:58:02+00:00 +size = 10724 + +[packages.wheels.hashes] +sha256 = "5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b" + +[[packages]] +name = "pytest" +version = "9.1.1" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz" +upload-time = 2026-06-19 10:58:32+00:00 +size = 1636369 + +[packages.sdist.hashes] +sha256 = "1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl" +upload-time = 2026-06-19 10:58:31+00:00 +size = 386536 + +[packages.wheels.hashes] +sha256 = "37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" [[packages]] -name = "mdurl" -version = "0.1.2" -marker = "\"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.7" +name = "pytest-httpserver" +version = "1.2.0" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "mdurl-0.1.2.tar.gz" -upload-time = 2022-08-14 12:40:10.846281+00:00 -url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" -size = 8729 +url = "https://files.pythonhosted.org/packages/37/ff/81c955e8fbb0323e817d178dd653de90c81896e819a5df65c1e1c327155a/pytest_httpserver-1.2.0.tar.gz" +upload-time = 2026-10-05 19:00:25+00:00 +size = 71860 [packages.sdist.hashes] -sha256 = "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" +sha256 = "d07744ce2efaafa0575ed3081d416b999c773b09e84aa95065138bedc3e12352" [[packages.wheels]] -name = "mdurl-0.1.2-py3-none-any.whl" -upload-time = 2022-08-14 12:40:09.779676+00:00 -url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" -size = 9979 +url = "https://files.pythonhosted.org/packages/8d/34/aa11718873f8cd3d9b3c58938e537af0c2d9cb108160357f5db2af6ddd66/pytest_httpserver-1.2.0-py3-none-any.whl" +upload-time = 2026-10-05 19:00:26+00:00 +size = 23392 [packages.wheels.hashes] -sha256 = "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" +sha256 = "749937892dbf909c8b703503b491be66fc24073e7988bf9b4d783af4f33da942" [[packages]] -name = "nab" -version = "0.0.22" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "nab-index" }, - { name = "nab-markersets" }, - { name = "nab-project" }, - { name = "nab-provider" }, - { name = "nab-resolver" }, - { name = "tomli" }, - { name = "typing-extensions" }, -] +name = "pytest-mock" +version = "3.16.0" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "nab-0.0.22.tar.gz" -upload-time = 2026-10-18 05:45:08.513928+00:00 -url = "https://files.pythonhosted.org/packages/25/3d/e4c8f4b2890efbf2fa6eef82acf57ecc0ef39b0a1e6843d306d2053a107c/nab-0.0.22.tar.gz" -size = 286128 +url = "https://files.pythonhosted.org/packages/7a/7f/6ed29931d5c8cd396e7c0a55412e6cc88020373365c8685985dea53d26d7/pytest_mock-3.16.0.tar.gz" +upload-time = 2026-09-27 14:57:55+00:00 +size = 35362 [packages.sdist.hashes] -sha256 = "a0da54ee96bf1eb1997581566620c250c8a3557139e9467ccaced2a12e9e71ac" +sha256 = "5a8395528b8f498205f3718f575228d0edaed7425fff638f87d1a6c3e0383636" [[packages.wheels]] -name = "nab-0.0.22-py3-none-any.whl" -upload-time = 2026-10-18 05:45:06.803974+00:00 -url = "https://files.pythonhosted.org/packages/56/4b/77baa0ddb0b7578b6475b2b578fa4579207b0f97e0b3b80480e01dcb3d6f/nab-0.0.22-py3-none-any.whl" -size = 137802 +url = "https://files.pythonhosted.org/packages/db/5b/b83a9bf1a3b4ec222f9fa083147ff6816245223da0ab92370e7e056f113f/pytest_mock-3.16.0-py3-none-any.whl" +upload-time = 2026-09-27 14:57:54+00:00 +size = 10016 [packages.wheels.hashes] -sha256 = "f62fcf75b28786fbf58627f6d7eaa735daefbc7f736715403bc4357acb57e7bb" +sha256 = "007cfeb257801d88d9c0b2a7b5a15a15e73b71968dfd72e7bf8c4a2f8393aec8" [[packages]] -name = "nab-index" -version = "0.0.22" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "nab-provider" }, - { name = "packaging" }, - { name = "truststore" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] +name = "pytest-xdist" +version = "3.8.0" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "nab_index-0.0.22.tar.gz" -upload-time = 2026-10-18 05:44:43.857925+00:00 -url = "https://files.pythonhosted.org/packages/2a/83/8d2483239b7dc5ac8d9f758db1cc19d4129d0143d34c2e1ab8cd48ea71fb/nab_index-0.0.22.tar.gz" -size = 110621 +url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz" +upload-time = 2025-07-01 13:30:59+00:00 +size = 88069 [packages.sdist.hashes] -sha256 = "5900ea5ad298512379cfddff1a73a544590f0b04620ef58ff345f99a7c59688e" +sha256 = "7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1" [[packages.wheels]] -name = "nab_index-0.0.22-py3-none-any.whl" -upload-time = 2026-10-18 05:44:42.188007+00:00 -url = "https://files.pythonhosted.org/packages/c0/58/c06ada29934a78479b989675829f26083972f38bf1e8a2503e01101cb970/nab_index-0.0.22-py3-none-any.whl" -size = 88373 +url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl" +upload-time = 2025-07-01 13:30:56+00:00 +size = 46396 [packages.wheels.hashes] -sha256 = "c89ffa607dfe4246dc59e9f5236d54413da3d310183ffa3298ba51a48e7c2ec7" +sha256 = "202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88" [[packages]] -name = "nab-markersets" -version = "0.0.22" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" +name = "python-discovery" +version = "1.6.3" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/fc/54/fad125f5c48f7821644ee6f223ebfcedc4e23597c1a16eeb13d33aa4a91a/python_discovery-1.6.3.tar.gz" +upload-time = 2026-10-16 03:54:29+00:00 +size = 88998 + +[packages.sdist.hashes] +sha256 = "a62b301d96cf5489cb96ab023b32e068421867fdd5df2e42eece877869047fa2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/de/ec/643826e4d69b7bdce4213f221e8f5ed06d0a9dd69fa9b33342578d2cc8c8/python_discovery-1.6.3-py3-none-any.whl" +upload-time = 2026-10-16 03:54:28+00:00 +size = 39386 + +[packages.wheels.hashes] +sha256 = "cb7654125e3dcb594269a6feb56ac693f1f13b7c654493a93eba72dc22d18034" + +[[packages]] +name = "pywin32-ctypes" +version = "0.2.3" +marker = "(\"test\" in dependency_groups and (platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'win32'))" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz" +upload-time = 2024-08-14 10:15:34+00:00 +size = 29471 + +[packages.sdist.hashes] +sha256 = "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl" +upload-time = 2024-08-14 10:15:33+00:00 +size = 30756 + +[packages.wheels.hashes] +sha256 = "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" + +[[packages]] +name = "pyyaml" +version = "6.0.3" +marker = "\"release-hook-check\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-shell\" in dependency_groups or \"release-validate\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz" +upload-time = 2025-09-25 21:33:16+00:00 +size = 130960 + +[packages.sdist.hashes] +sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" + +[[packages]] +name = "readme-renderer" +version = "46.0" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz" +upload-time = 2026-08-28 15:18:32+00:00 +size = 38382 + +[packages.sdist.hashes] +sha256 = "af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl" +upload-time = 2026-08-28 15:18:31+00:00 +size = 14239 + +[packages.wheels.hashes] +sha256 = "d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560" + +[[packages]] +name = "release-creator" +version = "1.1.0" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/cf/b2/ce67bd2fb46af74ddc5a96dcea9d04c2efa6eaa818a16aad956dd8bafce1/release_creator-1.1.0.tar.gz" +upload-time = 2026-10-24 09:08:39+00:00 +size = 195730 + +[packages.sdist.hashes] +sha256 = "26acad6c9885c148a9c0cb4fbec052c2b87ba1516ac3a337f3483c6fc606f749" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f9/b3/cab1d422780fd4077192b1aaba70558554c4844f04d5046e06034c593cf0/release_creator-1.1.0-py3-none-any.whl" +upload-time = 2026-10-24 09:08:38+00:00 +size = 62323 + +[packages.wheels.hashes] +sha256 = "fe3b60e46781b9bf5bddb6499727f3f14c73f12b9232382bffdb1817fa93c721" + +[[packages]] +name = "requests" +version = "2.34.2" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz" +upload-time = 2026-05-14 19:25:27+00:00 +size = 142856 + +[packages.sdist.hashes] +sha256 = "f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl" +upload-time = 2026-05-14 19:25:26+00:00 +size = 73075 + +[packages.wheels.hashes] +sha256 = "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" + +[[packages]] +name = "requests-toolbelt" +version = "1.0.0" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz" +upload-time = 2023-05-01 04:11:33+00:00 +size = 206888 + +[packages.sdist.hashes] +sha256 = "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl" +upload-time = 2023-05-01 04:11:28+00:00 +size = 54481 + +[packages.wheels.hashes] +sha256 = "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + +[[packages]] +name = "rfc3161-client" +version = "1.0.9" +marker = "\"release-attestations\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/56/53/e2f526fb6957023180c928448e2dd71b03b35038c315aed2d12d9a77c3af/rfc3161_client-1.0.9.tar.gz" +upload-time = 2026-09-14 19:32:29+00:00 +size = 112825 + +[packages.sdist.hashes] +sha256 = "66997415086d2c6e5d57a2d315157b32a9c3f4f1312877ca8eb55e2e0ddf8ce4" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/92/2c/b81b822bc067647aa291dc4ea71f052f994344934fc84d499759aec64cf8/rfc3161_client-1.0.9-cp39-abi3-macosx_10_12_x86_64.whl" +upload-time = 2026-09-14 19:32:08+00:00 +size = 2109721 + +[packages.wheels.hashes] +sha256 = "5e619d7e28ffa3a46685923e6d6ba4ce2e74cb09a41c77995a0702cc1a533bc4" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/88/cf/bcfb66eaeccdc076457451a6213360c840bcacd0e938e8507951be29373f/rfc3161_client-1.0.9-cp39-abi3-macosx_11_0_arm64.whl" +upload-time = 2026-09-14 19:32:10+00:00 +size = 2439706 + +[packages.wheels.hashes] +sha256 = "a7004edfbf1bb7ec7801978a39fa7a7fe385ca59673df90d7578079b6eba3b24" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/8e/f3/4b69d47fd7f79238bf8db20902a30d28c908efc0d906fb57abcc89ac75e3/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" +upload-time = 2026-09-14 19:32:12+00:00 +size = 2651455 + +[packages.wheels.hashes] +sha256 = "7b703b233dbee7228d117d5f1c118363c61d523fa6f018c9217c79e3c6684f86" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/33/49/154c3e80ac94e51b9d1097dab15b95f44aa593c395bd41775e4486a7d0a0/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" +upload-time = 2026-09-14 19:32:13+00:00 +size = 2051342 + +[packages.wheels.hashes] +sha256 = "d3cbf1991b6f3457efcb25e1d31179cac556112e6d3e65ec9e0fd70c6e9af90d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c8/57/79bf6a67cd4ca729b0b74e6fa79ebef84e6bc8657b85772df01a7d3b7b89/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl" +upload-time = 2026-09-14 19:32:15+00:00 +size = 2418228 + +[packages.wheels.hashes] +sha256 = "614da494c6f8850c5d6d98c23bc291a4e4dbf2c3c9082d9907fa156f9e4a75c6" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/11/ae/19139fee184916065705d660bad203b676dc40b449afff36f79818c59c4f/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-09-14 19:32:17+00:00 +size = 2415566 + +[packages.wheels.hashes] +sha256 = "fac3f440a507555e684dc5daba75e33dc08f0f45fdefa48448f19001233a6b21" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/6e/54/43f89c8e004baade51943261b29f9dd866df3383f4554d5797a2568c2855/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_aarch64.whl" +upload-time = 2026-09-14 19:32:19+00:00 +size = 2995975 + +[packages.wheels.hashes] +sha256 = "0ac247d5aa22f3514d1e284bdc547d2ad5f101e1e5d7f6db25db2ab3a0c59f06" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/2b/b2/938e4df767a2ea6fae2bc8e8e0df52e1d70a32a082a13bdfdd6fac59dc32/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_armv7l.whl" +upload-time = 2026-09-14 19:32:20+00:00 +size = 2358728 + +[packages.wheels.hashes] +sha256 = "03be7ac7919c7f4c566076b07773b81b1f1b28d447fa91c6960fb5f82fac5bc1" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ff/54/f0193b27f1a635aa336d19b48c296f133719cafb87e950de6eae4638a046/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_i686.whl" +upload-time = 2026-09-14 19:32:22+00:00 +size = 2565766 + +[packages.wheels.hashes] +sha256 = "8208999e94e1252e3f5f748f5efe59d3b9c308d04c7626be1becfbf3236c9de4" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/15/41/6c48b05fc41a424ab3b0cb0515e77046707b498837411633fe7ce77812ee/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_x86_64.whl" +upload-time = 2026-09-14 19:32:24+00:00 +size = 2643597 + +[packages.wheels.hashes] +sha256 = "d4cf92f0e784ff02ff5ea1c574f8521f30953dc178092424126dbd8348034f93" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d2/18/fcb2f311b0b1754b0476165a4eae2929606007fc0ff0a575a65dbff2c0f6/rfc3161_client-1.0.9-cp39-abi3-win32.whl" +upload-time = 2026-09-14 19:32:25+00:00 +size = 1996018 + +[packages.wheels.hashes] +sha256 = "4d33f5aaec2974239e1c62a79a6fcf32506b09095482377a8b5be3a00fa67dfb" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/d2/a1/b71d9f2071660a914e039061d3cdcad77b1cbccf7276bff073f95e8b4d1a/rfc3161_client-1.0.9-cp39-abi3-win_amd64.whl" +upload-time = 2026-09-14 19:32:27+00:00 +size = 2431351 + +[packages.wheels.hashes] +sha256 = "908f6a775da4bdce1d39f825a4431f389542696078ef3e4ed911a068bcaf1792" + +[[packages]] +name = "rfc3986" +version = "2.0.0" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz" +upload-time = 2022-01-10 00:52:30+00:00 +size = 49026 + +[packages.sdist.hashes] +sha256 = "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl" +upload-time = 2022-01-10 00:52:29+00:00 +size = 31326 + +[packages.wheels.hashes] +sha256 = "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd" + +[[packages]] +name = "rfc8785" +version = "0.1.4" +marker = "\"release-attestations\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/ef/2f/fa1d2e740c490191b572d33dbca5daa180cb423c24396b856f5886371d8b/rfc8785-0.1.4.tar.gz" +upload-time = 2024-09-27 16:33:31+00:00 +size = 14321 + +[packages.sdist.hashes] +sha256 = "e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/4d/78/119878110660b2ad709888c8a1614fce7e2fab39080ab960656dc8605bf6/rfc8785-0.1.4-py3-none-any.whl" +upload-time = 2024-09-27 16:33:29+00:00 +size = 9240 + +[packages.wheels.hashes] +sha256 = "520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48" + +[[packages]] +name = "rich" +version = "15.0.0" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz" +upload-time = 2026-04-12 08:24:00+00:00 +size = 230680 + +[packages.sdist.hashes] +sha256 = "edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl" +upload-time = 2026-04-12 08:24:02+00:00 +size = 310654 + +[packages.wheels.hashes] +sha256 = "33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb" + +[[packages]] +name = "secretstorage" +version = "3.5.0" +marker = "(\"test\" in dependency_groups and (platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux'))" index = "https://pypi.org/simple/" [packages.sdist] -name = "nab_markersets-0.0.22.tar.gz" -upload-time = 2026-10-18 05:44:19.580614+00:00 -url = "https://files.pythonhosted.org/packages/fc/b7/3997893dfdada9998d4dc786c10445a1361e197b4aed54ddd866c84dd538/nab_markersets-0.0.22.tar.gz" -size = 109258 +url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz" +upload-time = 2025-11-23 19:02:53+00:00 +size = 19884 [packages.sdist.hashes] -sha256 = "38212664f517f75d798808ecfe902bfd11fef340042ab0203100c7e06e78eea5" +sha256 = "f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be" [[packages.wheels]] -name = "nab_markersets-0.0.22-py3-none-any.whl" -upload-time = 2026-10-18 05:44:18.237004+00:00 -url = "https://files.pythonhosted.org/packages/5e/9e/9744adff3a66633a811f8253faaa8b91a447c99199edba16f2790e3647ea/nab_markersets-0.0.22-py3-none-any.whl" -size = 44360 +url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl" +upload-time = 2025-11-23 19:02:51+00:00 +size = 15554 [packages.wheels.hashes] -sha256 = "ad268dcece034495b2ea31a70250cd43cc4f6123039a548762c7ec8826e5d3bd" +sha256 = "0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137" [[packages]] -name = "nab-project" -version = "0.0.22" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "build" }, - { name = "installer" }, - { name = "nab-index" }, - { name = "nab-markersets" }, - { name = "nab-provider" }, - { name = "nab-resolver" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, - { name = "tomli" }, - { name = "tomli-w" }, - { name = "typing-extensions" }, -] +name = "securesystemslib" +version = "1.5.1" +marker = "\"release-attestations\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "nab_project-0.0.22.tar.gz" -upload-time = 2026-10-18 05:44:55.821489+00:00 -url = "https://files.pythonhosted.org/packages/65/84/f5197d4044ceda9c6ee3e7aefce154ab616fec632fa1d89cdfaacc665c07/nab_project-0.0.22.tar.gz" -size = 774409 +url = "https://files.pythonhosted.org/packages/81/d4/f9323507144693d869f30f13694035438ab6642153b94e3057945cf0584f/securesystemslib-1.5.1.tar.gz" +upload-time = 2026-09-04 06:55:32+00:00 +size = 947087 [packages.sdist.hashes] -sha256 = "5f7a1dee5b618149267b2b6233c3a220e45cd6f97bd5ec7f3f48fa7570e9f053" +sha256 = "4b8d00abd93707ead10b69eb2b8582376a1364de3b0a71077de534c2ef4985e0" [[packages.wheels]] -name = "nab_project-0.0.22-py3-none-any.whl" -upload-time = 2026-10-18 05:44:53.956127+00:00 -url = "https://files.pythonhosted.org/packages/de/c9/62affa4df6cabce48751307c4c3adfbaa8c54e402d464d015897a16a6dba/nab_project-0.0.22-py3-none-any.whl" -size = 148262 +url = "https://files.pythonhosted.org/packages/38/fc/c71335a5046d46bf3b761a8c7fa092b40a7721429a5613538f0a28ae80a6/securesystemslib-1.5.1-py3-none-any.whl" +upload-time = 2026-09-04 06:55:30+00:00 +size = 876390 [packages.wheels.hashes] -sha256 = "cb09fa143105fd99a60609b3ff4830924b0d4eaf5abad48678467ec4b73e577f" +sha256 = "ada8bdf817da29ece4ba91654f6a162ce7cfbadbc3ae3f840f7313f9d22675de" [[packages]] -name = "nab-provider" -version = "0.0.22" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "nab-markersets" }, - { name = "nab-resolver" }, - { name = "typing-extensions" }, -] +name = "setuptools" +version = "84.0.0" +marker = "\"build\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "nab_provider-0.0.22.tar.gz" -upload-time = 2026-10-18 05:44:31.579358+00:00 -url = "https://files.pythonhosted.org/packages/94/0d/64fbadae562fefc00257c4f5ec988171d6e6b3ea4363f1da2e8886e80228/nab_provider-0.0.22.tar.gz" -size = 409367 +url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz" +upload-time = 2026-08-08 18:27:58+00:00 +size = 1168449 [packages.sdist.hashes] -sha256 = "3f129ab7394bf5eb031152c3535ee74073e2a6b6ae081fb7f5a4d269fcf5f8a0" +sha256 = "f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73" [[packages.wheels]] -name = "nab_provider-0.0.22-py3-none-any.whl" -upload-time = 2026-10-18 05:44:29.803721+00:00 -url = "https://files.pythonhosted.org/packages/30/67/32bda18d34f0f8ec098f8e4a0f792164ef1a4b2aa61d435d20f4645572af/nab_provider-0.0.22-py3-none-any.whl" -size = 339242 +url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl" +upload-time = 2026-08-08 18:27:56+00:00 +size = 818216 [packages.wheels.hashes] -sha256 = "1450c9dc58b23da1a7bde7fdb43a72b043327bd0e3ef20435254ffcb8b7eae6b" +sha256 = "51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670" [[packages]] -name = "nab-resolver" -version = "0.0.22" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" +name = "sigstore" +version = "4.5.0" +marker = "\"release-attestations\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "nab_resolver-0.0.22.tar.gz" -upload-time = 2026-10-18 05:44:08.017044+00:00 -url = "https://files.pythonhosted.org/packages/84/bc/dcbfe5163538b1cc8421797e3f8ff77d79321cbc9943af8065e2c8ae1c09/nab_resolver-0.0.22.tar.gz" -size = 167648 +url = "https://files.pythonhosted.org/packages/18/e0/279419065e2d7102413605b3456122adbbccbc42e010b499c7b882fc01f8/sigstore-4.5.0.tar.gz" +upload-time = 2026-07-28 07:34:01+00:00 +size = 90969 [packages.sdist.hashes] -sha256 = "e005b225213e032858cf4aef3850961aaa1d4ea521428da731f33175ebd3b05f" +sha256 = "020d3e07f622b2916bf453e66ff6ff0711e1fdc5ab69e8bd8902f71d9fcb316f" [[packages.wheels]] -name = "nab_resolver-0.0.22-py3-none-any.whl" -upload-time = 2026-10-18 05:44:06.445864+00:00 -url = "https://files.pythonhosted.org/packages/22/a8/6e30b25ae12e0c662dc9b181a7f32fa8cd22f26016f12dc1d10176f62bba/nab_resolver-0.0.22-py3-none-any.whl" -size = 58597 +url = "https://files.pythonhosted.org/packages/bf/7f/51dc313c06dd3ec6c5b308e334492cfdea0b02c9184a0da7b2db8c2a30a1/sigstore-4.5.0-py3-none-any.whl" +upload-time = 2026-07-28 07:34:00+00:00 +size = 111724 [packages.wheels.hashes] -sha256 = "6f77daa98279ef0eedb9e8f2248aee4c50e01b92b470b68886995e5056d79971" +sha256 = "f045b207f2e12605cf775ec38e89c5eda625d71ffa7830477db65e47ec2bc8b2" [[packages]] -name = "packaging" -version = "26.3" -marker = "\"lock-reader\" in dependency_groups or \"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.9" +name = "sigstore-models" +version = "0.0.6" +marker = "\"release-attestations\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "packaging-26.3.tar.gz" -upload-time = 2026-08-04 18:15:28.737071+00:00 -url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz" -size = 313412 +url = "https://files.pythonhosted.org/packages/c6/ed/5c0ff809f90b19f4e971e17c1ed11f4df60082c6010b32a82054087e91e0/sigstore_models-0.0.6.tar.gz" +upload-time = 2025-11-26 19:18:12+00:00 +size = 7037 [packages.sdist.hashes] -sha256 = "94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79" +sha256 = "c766c09470c2a7e8a4a333c893f07e2001c56a3ff1757b1a246119f53169a849" [[packages.wheels]] -name = "packaging-26.3-py3-none-any.whl" -upload-time = 2026-08-04 18:15:27.159957+00:00 -url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl" -size = 129956 +url = "https://files.pythonhosted.org/packages/12/dc/7a19864a7bc9f43121ab459e12768ab0080590e3e1b60a29b2f2eb01fb85/sigstore_models-0.0.6-py3-none-any.whl" +upload-time = 2025-11-26 19:18:11+00:00 +size = 13213 [packages.wheels.hashes] -sha256 = "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" +sha256 = "5201a68f4d7d0f8bec1e2f4378eb646b084c52609a4e31db8c385095fff68b2e" [[packages]] -name = "platformdirs" -version = "4.13.3" +name = "sigstore-rekor-types" +version = "0.0.18" marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.11" index = "https://pypi.org/simple/" [packages.sdist] -name = "platformdirs-4.13.3.tar.gz" -upload-time = 2026-10-16 01:16:16.573321+00:00 -url = "https://files.pythonhosted.org/packages/da/91/4a03cfdb03314cfca262921797fff04ff4054bd86a160aed76eddf8aa12b/platformdirs-4.13.3.tar.gz" -size = 61559 +url = "https://files.pythonhosted.org/packages/b4/54/102e772445c5e849b826fbdcd44eb9ad7b3d10fda17b08964658ec7027dc/sigstore_rekor_types-0.0.18.tar.gz" +upload-time = 2024-11-22 13:59:54+00:00 +size = 15687 [packages.sdist.hashes] -sha256 = "5e567f664eb087ab8521c0179cd8d1bd60857d271136567a39719e28e2d383ce" +sha256 = "19aef25433218ebf9975a1e8b523cc84aaf3cd395ad39a30523b083ea7917ec5" [[packages.wheels]] -name = "platformdirs-4.13.3-py3-none-any.whl" -upload-time = 2026-10-16 01:16:15.051988+00:00 -url = "https://files.pythonhosted.org/packages/45/b8/fd1af06b079af236f5423f7c1821264419cc8f6b4803f79353acbb8bfa53/platformdirs-4.13.3-py3-none-any.whl" -size = 32762 +url = "https://files.pythonhosted.org/packages/45/7c/f0b4e19fd424df4cc964f5d454e1d814fd2dc3b386342e6040441024318f/sigstore_rekor_types-0.0.18-py3-none-any.whl" +upload-time = 2024-11-22 13:59:52+00:00 +size = 20610 [packages.wheels.hashes] -sha256 = "f6ad7f447f24f8a3b82cce5976387428bff894a0eca6c3488f4a17f153c130c4" +sha256 = "b62bf38c5b1a62bc0d7fe0ee51a0709e49311d137c7880c329882a8f4b2d1d78" [[packages]] -name = "prek" -version = "0.5.5" -marker = "\"release-check\" in dependency_groups or \"release-hook-check\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-shell\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.8" +name = "tomli" +version = "2.5.0" +marker = "\"lock-tools\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "prek-0.5.5.tar.gz" -upload-time = 2026-10-05 06:08:59.165911+00:00 -url = "https://files.pythonhosted.org/packages/9b/7b/11538b8e6cd7e269261feae62a2d08818fdcf8f43c3b1a253cec01a5488d/prek-0.5.5.tar.gz" -size = 570428 +url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz" +upload-time = 2026-10-07 12:23:37+00:00 +size = 17662 [packages.sdist.hashes] -sha256 = "16bf596768bb754779c7d42317e9ca16ac609c0cce7d13bae0829944ab40a26a" +sha256 = "264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6" [[packages.wheels]] -name = "prek-0.5.5-py3-none-macosx_10_12_x86_64.whl" -upload-time = 2026-10-05 06:08:39.368112+00:00 -url = "https://files.pythonhosted.org/packages/2e/77/189e554ea92baf6c1d33733b3e9d1ed819ab7d77e57eb2e3ead0bb1d8060/prek-0.5.5-py3-none-macosx_10_12_x86_64.whl" -size = 6038669 +url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl" +upload-time = 2026-10-07 12:23:13+00:00 +size = 162775 [packages.wheels.hashes] -sha256 = "50fbceb0f728b504023f6e6c9037ac0656436949d94f7a85bac5542e6c1a4a75" +sha256 = "a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc" [[packages.wheels]] -name = "prek-0.5.5-py3-none-macosx_11_0_arm64.whl" -upload-time = 2026-10-05 06:08:41.428822+00:00 -url = "https://files.pythonhosted.org/packages/63/ea/fc857c6b3f1e3fdb4d81bdca2051076de82e986cb1455a01b351ac7a5cd1/prek-0.5.5-py3-none-macosx_11_0_arm64.whl" -size = 5548955 +url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl" +upload-time = 2026-10-07 12:23:15+00:00 +size = 161406 [packages.wheels.hashes] -sha256 = "2f610927b95e84b28173a4c810892a7b5d5bb55bee792ddf97178fa97654633e" +sha256 = "9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276" [[packages.wheels]] -name = "prek-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl" -upload-time = 2026-10-05 06:08:44.051128+00:00 -url = "https://files.pythonhosted.org/packages/47/c3/2bab54f6393fb9f247225681d476086e24fa6242f329e9ca1e5bd638ec50/prek-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl" -size = 5848228 +url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-10-07 12:23:16+00:00 +size = 273855 [packages.wheels.hashes] -sha256 = "120ef7d4b6bb58155e74827743c266e48501efb0e4c37ff726b14fe6e86a126b" +sha256 = "752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52" [[packages.wheels]] -name = "prek-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" -upload-time = 2026-10-05 06:08:48.645837+00:00 -url = "https://files.pythonhosted.org/packages/93/fc/e566be5919eaffef4cc22488f16e4bbfd08961734bd67c06f85d405e4cbc/prek-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" -size = 6269922 +url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-10-07 12:23:18+00:00 +size = 284910 [packages.wheels.hashes] -sha256 = "73d509c0d9556171de2502bada4962be7a25871e7b23e782549074e1416dc850" +sha256 = "c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7" [[packages.wheels]] -name = "prek-0.5.5-py3-none-manylinux_2_28_aarch64.whl" -upload-time = 2026-10-05 06:08:50.808055+00:00 -url = "https://files.pythonhosted.org/packages/d2/39/a48d8e6203055bff7bb734b1f2f9aede4a4539bee6ab4c9273dbc01fa546/prek-0.5.5-py3-none-manylinux_2_28_aarch64.whl" -size = 5861518 +url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl" +upload-time = 2026-10-07 12:23:19+00:00 +size = 277723 [packages.wheels.hashes] -sha256 = "76c867fdbd4e4f19e6b6b2988bd1e1407d1324170ebcf1428e4e34f8f0751a99" +sha256 = "19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391" [[packages.wheels]] -name = "prek-0.5.5-py3-none-win_amd64.whl" -upload-time = 2026-10-05 06:08:55.056244+00:00 -url = "https://files.pythonhosted.org/packages/8c/04/1e9ccf3a61a3bfd4107b83973f7afde9177b6ff8f2d5d4b00125e77c2100/prek-0.5.5-py3-none-win_amd64.whl" -size = 5784759 +url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl" +upload-time = 2026-10-07 12:23:20+00:00 +size = 285115 [packages.wheels.hashes] -sha256 = "15bff9039349466ea9bf6b0c173b822ff08f8ae4cfbfdea782492e519c06d874" +sha256 = "57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859" [[packages.wheels]] -name = "prek-0.5.5-py3-none-win_arm64.whl" -upload-time = 2026-10-05 06:08:57.193388+00:00 -url = "https://files.pythonhosted.org/packages/41/d9/bb6811e438d3c74d26cb5d72e6343606e0a7a6283fee87521189f4ba1191/prek-0.5.5-py3-none-win_arm64.whl" -size = 5517355 +url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl" +upload-time = 2026-10-07 12:23:21+00:00 +size = 103475 [packages.wheels.hashes] -sha256 = "3db3191d2509830c75438e20956a47c2259867d18a4106064ed75088b968a0c6" - -[[packages]] -name = "pyasn1" -version = "0.6.4" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.8" -index = "https://pypi.org/simple/" +sha256 = "667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb" -[packages.sdist] -name = "pyasn1-0.6.4.tar.gz" -upload-time = 2026-07-09 01:12:33.988388+00:00 -url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz" -size = 151262 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl" +upload-time = 2026-10-07 12:23:23+00:00 +size = 114589 -[packages.sdist.hashes] -sha256 = "9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81" +[packages.wheels.hashes] +sha256 = "d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5" [[packages.wheels]] -name = "pyasn1-0.6.4-py3-none-any.whl" -upload-time = 2026-07-09 01:12:32.920042+00:00 -url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl" -size = 84410 +url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl" +upload-time = 2026-10-07 12:23:24+00:00 +size = 111493 [packages.wheels.hashes] -sha256 = "deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b" - -[[packages]] -name = "pycparser" -version = "3.11" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -index = "https://pypi.org/simple/" +sha256 = "75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd" -[packages.sdist] -name = "pycparser-3.11.tar.gz" -upload-time = 2026-10-09 12:56:59.539345+00:00 -url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz" -size = 113796 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl" +upload-time = 2026-10-07 12:23:25+00:00 +size = 171380 -[packages.sdist.hashes] -sha256 = "d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc" +[packages.wheels.hashes] +sha256 = "2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57" [[packages.wheels]] -name = "pycparser-3.11-py3-none-any.whl" -upload-time = 2026-10-09 12:56:58.131173+00:00 -url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl" -size = 51178 +url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl" +upload-time = 2026-10-07 12:23:26+00:00 +size = 170553 [packages.wheels.hashes] -sha256 = "51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80" - -[[packages]] -name = "pydantic" -version = "2.14.1" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "annotated-types" }, - { name = "email-validator" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -index = "https://pypi.org/simple/" +sha256 = "0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd" -[packages.sdist] -name = "pydantic-2.14.1.tar.gz" -upload-time = 2026-10-11 18:37:55.396662+00:00 -url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz" -size = 900362 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" +upload-time = 2026-10-07 12:23:28+00:00 +size = 294428 -[packages.sdist.hashes] -sha256 = "94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26" +[packages.wheels.hashes] +sha256 = "49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01" [[packages.wheels]] -name = "pydantic-2.14.1-py3-none-any.whl" -upload-time = 2026-10-11 18:37:53.437059+00:00 -url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl" -size = 488106 +url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-10-07 12:23:29+00:00 +size = 304909 [packages.wheels.hashes] -sha256 = "9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454" +sha256 = "b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f" -[[packages]] -name = "pydantic-core" -version = "2.50.1" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "typing-extensions" }, -] -index = "https://pypi.org/simple/" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl" +upload-time = 2026-10-07 12:23:30+00:00 +size = 293220 -[packages.sdist] -name = "pydantic_core-2.50.1.tar.gz" -upload-time = 2026-10-11 18:35:44.820993+00:00 -url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz" -size = 513515 +[packages.wheels.hashes] +sha256 = "b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a" -[packages.sdist.hashes] -sha256 = "e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl" +upload-time = 2026-10-07 12:23:32+00:00 +size = 305705 + +[packages.wheels.hashes] +sha256 = "8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142" [[packages.wheels]] -name = "pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl" -upload-time = 2026-10-11 18:34:04.016846+00:00 -url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl" -size = 2224599 +url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl" +upload-time = 2026-10-07 12:23:33+00:00 +size = 108432 [packages.wheels.hashes] -sha256 = "8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102" +sha256 = "62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5" [[packages.wheels]] -name = "pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl" -upload-time = 2026-10-11 18:34:05.901276+00:00 -url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl" -size = 2024191 +url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl" +upload-time = 2026-10-07 12:23:34+00:00 +size = 117281 [packages.wheels.hashes] -sha256 = "84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c" +sha256 = "f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571" [[packages.wheels]] -name = "pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" -upload-time = 2026-10-11 18:34:07.757584+00:00 -url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" -size = 2048252 +url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl" +upload-time = 2026-10-07 12:23:35+00:00 +size = 115069 [packages.wheels.hashes] -sha256 = "23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7" +sha256 = "d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7" [[packages.wheels]] -name = "pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" -upload-time = 2026-10-11 18:34:15.560294+00:00 -url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" -size = 2159961 +url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl" +upload-time = 2026-10-07 12:23:36+00:00 +size = 14765 [packages.wheels.hashes] -sha256 = "7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa" +sha256 = "32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b" + +[[packages]] +name = "tomli-w" +version = "1.2.0" +marker = "\"lock-tools\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz" +upload-time = 2025-01-15 12:07:24+00:00 +size = 7184 + +[packages.sdist.hashes] +sha256 = "2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021" [[packages.wheels]] -name = "pydantic_core-2.50.1-cp315-cp315-win_amd64.whl" -upload-time = 2026-10-11 18:34:29.508272+00:00 -url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl" -size = 2171681 +url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl" +upload-time = 2025-01-15 12:07:22+00:00 +size = 6675 [packages.wheels.hashes] -sha256 = "983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed" +sha256 = "188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90" + +[[packages]] +name = "tomlkit" +version = "0.15.2" +marker = "\"lock-tools\" in dependency_groups or \"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups or \"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/08/72/fc01563df95c189a9f65850096bdc205bc2fe37da6c740cd64afaf18d07e/tomlkit-0.15.2.tar.gz" +upload-time = 2026-10-16 04:07:08+00:00 +size = 190522 + +[packages.sdist.hashes] +sha256 = "83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6" [[packages.wheels]] -name = "pydantic_core-2.50.1-cp315-cp315-win_arm64.whl" -upload-time = 2026-10-11 18:34:31.455313+00:00 -url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl" -size = 2156251 +url = "https://files.pythonhosted.org/packages/da/46/7d84138a60a8bd796825b2d38ebe92d0d16501ca57d501b7c5d677401bbc/tomlkit-0.15.2-py3-none-any.whl" +upload-time = 2026-10-16 04:07:09+00:00 +size = 52225 [packages.wheels.hashes] -sha256 = "94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54" +sha256 = "05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437" [[packages]] -name = "pygments" -version = "2.21.0" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.9" +name = "towncrier" +version = "26.9.0" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "pygments-2.21.0.tar.gz" -upload-time = 2026-08-17 08:02:48.824391+00:00 -url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz" -size = 5005329 +url = "https://files.pythonhosted.org/packages/92/d4/e85ce614bf45b1a7fe13b82cc687819c2eaac5a1dd94b84834322095fd98/towncrier-26.9.0.tar.gz" +upload-time = 2026-09-04 12:57:25+00:00 +size = 79338 [packages.sdist.hashes] -sha256 = "610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" +sha256 = "ace9031631c718cc0709107b8755e6e31350eb8acdc877af43df3177addb9a55" [[packages.wheels]] -name = "pygments-2.21.0-py3-none-any.whl" -upload-time = 2026-08-17 08:02:44.912148+00:00 -url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl" -size = 1250147 +url = "https://files.pythonhosted.org/packages/82/88/f9340b545dafa64de053798e760515a2736fbb60092cf9aa6b146ccf3ede/towncrier-26.9.0-py3-none-any.whl" +upload-time = 2026-09-04 12:57:23+00:00 +size = 67030 [packages.wheels.hashes] -sha256 = "2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9" +sha256 = "ae4d223e6aadaff98d29b7f09e58d9c9ccf4cd3b455a2d2e0486d432cd8bf660" [[packages]] -name = "pyjwt" -version = "2.15.1" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.9" +name = "tox" +version = "4.66.0" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "pyjwt-2.15.1.tar.gz" -upload-time = 2026-09-28 18:40:42.598678+00:00 -url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz" -size = 121252 +url = "https://files.pythonhosted.org/packages/65/17/183426a492c45f113fb73e26d7e17de63222338222a887b1da055356d0bc/tox-4.66.0.tar.gz" +upload-time = 2026-10-16 08:09:14+00:00 +size = 344679 [packages.sdist.hashes] -sha256 = "4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8" +sha256 = "f9dc2cec9310a3ab5a560669cf860cf6ffad7d04733a0845c7ad7ac00d6b2808" [[packages.wheels]] -name = "pyjwt-2.15.1-py3-none-any.whl" -upload-time = 2026-09-28 18:40:41.429003+00:00 -url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl" -size = 33860 +url = "https://files.pythonhosted.org/packages/b9/e4/929ad087f3f4c6c8dadf64251d7215da77c0b2b6628b8e55c9b2fbcb5d72/tox-4.66.0-py3-none-any.whl" +upload-time = 2026-10-16 08:09:12+00:00 +size = 237594 [packages.wheels.hashes] -sha256 = "42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193" +sha256 = "f12458756246e8be78934f011024dc952c25631008c7a935683fe5843408add4" [[packages]] -name = "pyopenssl" -version = "26.4.0" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.9" -dependencies = [ - { name = "cryptography" }, -] +name = "tox-uv" +version = "1.37.0" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/2a/65/7cf72148bff33fd0a572e620c7b1573d6a5e4812e9207878d4e29e4a938a/tox_uv-1.37.0-py3-none-any.whl" +upload-time = 2026-10-11 00:33:49+00:00 +size = 6564 + +[packages.wheels.hashes] +sha256 = "f2e7de8f12d7ffd850d2a3dcebf278cf72a8952a9abe75d84cfb54a76ee17ae4" + +[[packages]] +name = "tox-uv-bare" +version = "1.37.0" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "pyopenssl-26.4.0.tar.gz" -upload-time = 2026-08-01 19:50:50.512064+00:00 -url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz" -size = 182046 +url = "https://files.pythonhosted.org/packages/31/27/203b4dbe73cc6dadc2480e319631310981540dabe95893de983106d7b103/tox_uv_bare-1.37.0.tar.gz" +upload-time = 2026-10-11 00:33:52+00:00 +size = 32961 [packages.sdist.hashes] -sha256 = "28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7" +sha256 = "97418f2febec7da57d010ca6ffed8c7d56be9d425f381b74c7fe8d0213fccad9" [[packages.wheels]] -name = "pyopenssl-26.4.0-py3-none-any.whl" -upload-time = 2026-08-01 19:50:48.940195+00:00 -url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl" -size = 56026 +url = "https://files.pythonhosted.org/packages/52/4c/9596668bb448b868c684a3df9190103e9bc2f7de13e646628ac914f928be/tox_uv_bare-1.37.0-py3-none-any.whl" +upload-time = 2026-10-11 00:33:51+00:00 +size = 22322 [packages.wheels.hashes] -sha256 = "f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c" +sha256 = "a596c0726690e2182cc85dfbc2f39e6fddacb8f7ea08bf8bace122a17397cd13" [[packages]] -name = "pypi-attestations" -version = "0.0.30" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "cryptography" }, - { name = "packaging" }, - { name = "pyasn1" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "rfc3986" }, - { name = "sigstore" }, - { name = "sigstore-models" }, -] +name = "trove-classifiers" +version = "2026.9.21.13" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "pypi_attestations-0.0.30.tar.gz" -upload-time = 2026-07-28 14:08:35.011386+00:00 -url = "https://files.pythonhosted.org/packages/97/1e/eb8a0233cdcaf01acb18076ac8b8a057714177b3717baf26b78a11b8d929/pypi_attestations-0.0.30.tar.gz" -size = 139108 +url = "https://files.pythonhosted.org/packages/bf/93/af436dfaa845cab5d96f0adbc1e4f3730532d37fa249e4eb796fb1d7fc82/trove_classifiers-2026.9.21.13.tar.gz" +upload-time = 2026-09-21 13:29:07+00:00 +size = 17085 [packages.sdist.hashes] -sha256 = "14ff13c76bbef02a483c2b77532da02bfd746b5141b881f355e06b9807434423" +sha256 = "0a9ebc8d4e2f3e8a22848c5258033035bec17a3012ac3fea16dbaa764489eb71" [[packages.wheels]] -name = "pypi_attestations-0.0.30-py3-none-any.whl" -upload-time = 2026-07-28 14:08:33.453745+00:00 -url = "https://files.pythonhosted.org/packages/5f/24/e59078318b5e2bca59be5a21957d70e082e6eb4f478adffe373f3b74daf4/pypi_attestations-0.0.30-py3-none-any.whl" -size = 23015 +url = "https://files.pythonhosted.org/packages/30/81/0da8afb52a71d0a4f2bd3152357b1a441e393b286374802b9d3addab4ab5/trove_classifiers-2026.9.21.13-py3-none-any.whl" +upload-time = 2026-09-21 13:29:06+00:00 +size = 14232 [packages.wheels.hashes] -sha256 = "b3a9c53f6cb89e5e7b5b70e6cfca97cfc66008c1ed54087355e06e40071cef21" +sha256 = "8b1ff4f9c191b1040b71c37f1e445ab99732911e3cd91de52838453a854d7a17" [[packages]] -name = "pyproject-hooks" -version = "1.3.3" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.8" +name = "trustme" +version = "1.2.1" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "pyproject_hooks-1.3.3.tar.gz" -upload-time = 2026-09-16 08:58:03.999217+00:00 -url = "https://files.pythonhosted.org/packages/6d/5d/f2ddeef4a855a102aaae5e97826a0260007522ab504421b75addfdb1517c/pyproject_hooks-1.3.3.tar.gz" -size = 21013 +url = "https://files.pythonhosted.org/packages/4b/c5/931476f4cf1cd9e736f32651005078061a50dc164a2569fb874e00eb2786/trustme-1.2.1.tar.gz" +upload-time = 2025-01-02 01:55:32+00:00 +size = 26844 [packages.sdist.hashes] -sha256 = "defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43" +sha256 = "6528ba2bbc7f2db41f33825c8dd13e3e3eb9d334ba0f909713c8c3139f4ae47f" [[packages.wheels]] -name = "pyproject_hooks-1.3.3-py3-none-any.whl" -upload-time = 2026-09-16 08:58:02.960279+00:00 -url = "https://files.pythonhosted.org/packages/85/11/044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977/pyproject_hooks-1.3.3-py3-none-any.whl" -size = 10724 +url = "https://files.pythonhosted.org/packages/b5/f3/c34dbabf6da5eda56fe923226769d40e11806952cd7f46655dd06e10f018/trustme-1.2.1-py3-none-any.whl" +upload-time = 2025-01-02 01:55:30+00:00 +size = 16530 [packages.wheels.hashes] -sha256 = "5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b" +sha256 = "d768e5fc57c86dfc5ec9365102e9b092541cd6954b35d8c1eea01a84f35a762a" [[packages]] -name = "pyyaml" -version = "6.0.3" -marker = "\"release-hook-check\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-shell\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.8" +name = "truststore" +version = "0.10.5" +marker = "\"lock-tools\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "pyyaml-6.0.3.tar.gz" -upload-time = 2025-09-25 21:33:16.546886+00:00 -url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz" -size = 130960 +url = "https://files.pythonhosted.org/packages/ee/9f/c5201d42a484c061e528825fc8e2d565f5abd50a4ced6fb7d29c4ec99b2b/truststore-0.10.5.tar.gz" +upload-time = 2026-10-12 22:27:31+00:00 +size = 28091 [packages.sdist.hashes] -sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" +sha256 = "30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/51/e9/3a7820be2bb0fe53b6bc9c3be26d3d1158004e4c3ab953aa6840b955b1e9/truststore-0.10.5-py3-none-any.whl" +upload-time = 2026-10-12 22:27:30+00:00 +size = 19017 + +[packages.wheels.hashes] +sha256 = "9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c" [[packages]] -name = "release-creator" -version = "1.0.0" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.15" -dependencies = [ - { name = "docutils" }, - { name = "markdown-it-py" }, - { name = "mdformat" }, - { name = "mdformat-gfm" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "towncrier" }, -] +name = "tuf" +version = "7.0.1" +marker = "\"release-attestations\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "release_creator-1.0.0.tar.gz" -upload-time = 2026-10-24 06:07:45.789370+00:00 -url = "https://files.pythonhosted.org/packages/e2/92/dfc1c6e8f52d50d91a05f77f67592c2408cabd9d96079fedbe551f24530d/release_creator-1.0.0.tar.gz" -size = 134186 +url = "https://files.pythonhosted.org/packages/9c/dd/52e7390cbac308e6b1cfe6be9bc3a96fe26326d5893cc297a52bb72792a9/tuf-7.0.1.tar.gz" +upload-time = 2026-09-02 06:19:52+00:00 +size = 274148 [packages.sdist.hashes] -sha256 = "1174e3a3fd39e9117eb9c34e3fbc58a8e32c49f5f65d01f39195231b967807f6" +sha256 = "5ada1db78da3518fa851e28588f4b2249b8662a28ae528f54afa2a1de541fb31" [[packages.wheels]] -name = "release_creator-1.0.0-py3-none-any.whl" -upload-time = 2026-10-24 06:07:44.366184+00:00 -url = "https://files.pythonhosted.org/packages/6a/17/b1cd851932786f3e9e9f2187d150bc0860e04adf76cbfd45e6531715bf97/release_creator-1.0.0-py3-none-any.whl" -size = 53396 +url = "https://files.pythonhosted.org/packages/a2/10/171e306e4a19ff980b9029ce5a523881e34bc6a2a52750fb1853f49f0fb9/tuf-7.0.1-py3-none-any.whl" +upload-time = 2026-09-02 06:19:50+00:00 +size = 56253 [packages.wheels.hashes] -sha256 = "613a26a0d1b0042d9dad60dfc00f3e78208d84ab9f78cec792a407b6a0a75c24" +sha256 = "d30434bda6e079ab303fb30d1b3006d939a10ca34783b1573d61cd9b802fa45c" [[packages]] -name = "requests" -version = "2.34.2" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] +name = "twine" +version = "7.0.0" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "requests-2.34.2.tar.gz" -upload-time = 2026-05-14 19:25:27.735762+00:00 -url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz" -size = 142856 +url = "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz" +upload-time = 2026-07-27 15:59:00+00:00 +size = 215032 [packages.sdist.hashes] -sha256 = "f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" +sha256 = "85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177" [[packages.wheels]] -name = "requests-2.34.2-py3-none-any.whl" -upload-time = 2026-05-14 19:25:26.443000+00:00 -url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl" -size = 73075 +url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl" +upload-time = 2026-07-27 15:58:59+00:00 +size = 43204 [packages.wheels.hashes] -sha256 = "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" +sha256 = "b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7" [[packages]] -name = "rfc3161-client" -version = "1.0.9" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.9" -dependencies = [ - { name = "cryptography" }, -] +name = "ty" +version = "0.0.89" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "rfc3161_client-1.0.9.tar.gz" -upload-time = 2026-09-14 19:32:29.113898+00:00 -url = "https://files.pythonhosted.org/packages/56/53/e2f526fb6957023180c928448e2dd71b03b35038c315aed2d12d9a77c3af/rfc3161_client-1.0.9.tar.gz" -size = 112825 +url = "https://files.pythonhosted.org/packages/67/ea/dfb9bdbc168c50a568809466596751afa0c7da60da03b8c12906d1decde4/ty-0.0.89.tar.gz" +upload-time = 2026-10-16 10:35:05+00:00 +size = 7978241 [packages.sdist.hashes] -sha256 = "66997415086d2c6e5d57a2d315157b32a9c3f4f1312877ca8eb55e2e0ddf8ce4" +sha256 = "bc1027b1ea95cca25c2a3a0b41433c809cf4b86a384d2b2434eba4bcd34548d4" [[packages.wheels]] -name = "rfc3161_client-1.0.9-cp39-abi3-macosx_10_12_x86_64.whl" -upload-time = 2026-09-14 19:32:08.154837+00:00 -url = "https://files.pythonhosted.org/packages/92/2c/b81b822bc067647aa291dc4ea71f052f994344934fc84d499759aec64cf8/rfc3161_client-1.0.9-cp39-abi3-macosx_10_12_x86_64.whl" -size = 2109721 +url = "https://files.pythonhosted.org/packages/c5/84/e199d9a3dc3c94c804ddd9e94b63dbe543ae7f39329574963b6a6a95872f/ty-0.0.89-py3-none-linux_armv6l.whl" +upload-time = 2026-10-16 10:34:24+00:00 +size = 14254879 [packages.wheels.hashes] -sha256 = "5e619d7e28ffa3a46685923e6d6ba4ce2e74cb09a41c77995a0702cc1a533bc4" +sha256 = "739b087e8873e177ffda99ff8a3600a4a7704d06523a8e88c4493021eee40b8a" [[packages.wheels]] -name = "rfc3161_client-1.0.9-cp39-abi3-macosx_11_0_arm64.whl" -upload-time = 2026-09-14 19:32:10.339509+00:00 -url = "https://files.pythonhosted.org/packages/88/cf/bcfb66eaeccdc076457451a6213360c840bcacd0e938e8507951be29373f/rfc3161_client-1.0.9-cp39-abi3-macosx_11_0_arm64.whl" -size = 2439706 +url = "https://files.pythonhosted.org/packages/e5/d5/5afe09bf5c83c4e3a9e5913386a460e240a7029f2ae5fc96df5b43563a91/ty-0.0.89-py3-none-macosx_10_12_x86_64.whl" +upload-time = 2026-10-16 10:34:27+00:00 +size = 13790417 [packages.wheels.hashes] -sha256 = "a7004edfbf1bb7ec7801978a39fa7a7fe385ca59673df90d7578079b6eba3b24" +sha256 = "47ebc98480c8a301955e18a8bdd67ef07090071ad47349b3254f325d3d5ab4e7" [[packages.wheels]] -name = "rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" -upload-time = 2026-09-14 19:32:12.025939+00:00 -url = "https://files.pythonhosted.org/packages/8e/f3/4b69d47fd7f79238bf8db20902a30d28c908efc0d906fb57abcc89ac75e3/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" -size = 2651455 +url = "https://files.pythonhosted.org/packages/7e/f9/88648f0ba9400dfd642c1e8dc0aeecb87945b121f3ff51f3902883c016d6/ty-0.0.89-py3-none-macosx_11_0_arm64.whl" +upload-time = 2026-10-16 10:34:29+00:00 +size = 12856900 [packages.wheels.hashes] -sha256 = "7b703b233dbee7228d117d5f1c118363c61d523fa6f018c9217c79e3c6684f86" +sha256 = "b0631291af5c850a15007c0ffb906d813966466e4ec12173c44977c0c5a2d51d" [[packages.wheels]] -name = "rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" -upload-time = 2026-09-14 19:32:17.185308+00:00 -url = "https://files.pythonhosted.org/packages/11/ae/19139fee184916065705d660bad203b676dc40b449afff36f79818c59c4f/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" -size = 2415566 +url = "https://files.pythonhosted.org/packages/6a/89/baae2b407101c4cb02f19216123900c147af8fd87cdb1ed7eba139472734/ty-0.0.89-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" +upload-time = 2026-10-16 10:34:32+00:00 +size = 13653330 [packages.wheels.hashes] -sha256 = "fac3f440a507555e684dc5daba75e33dc08f0f45fdefa48448f19001233a6b21" +sha256 = "3d768300ae16274ce656006e9199869ad68684b74400d963e1c82deddf3a22ad" [[packages.wheels]] -name = "rfc3161_client-1.0.9-cp39-abi3-win_amd64.whl" -upload-time = 2026-09-14 19:32:27.621426+00:00 -url = "https://files.pythonhosted.org/packages/d2/a1/b71d9f2071660a914e039061d3cdcad77b1cbccf7276bff073f95e8b4d1a/rfc3161_client-1.0.9-cp39-abi3-win_amd64.whl" -size = 2431351 +url = "https://files.pythonhosted.org/packages/ed/1d/293516c2e10a77134cfc40d22c3fc08edd8c5eadae9c75531ec8a9263348/ty-0.0.89-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" +upload-time = 2026-10-16 10:34:34+00:00 +size = 13986215 [packages.wheels.hashes] -sha256 = "908f6a775da4bdce1d39f825a4431f389542696078ef3e4ed911a068bcaf1792" - -[[packages]] -name = "rfc3986" -version = "2.0.0" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.7" -index = "https://pypi.org/simple/" +sha256 = "3dd4c57ba58bab8f6c816b2c254db52b58834b5dbf8268287382f7a2f9735c8f" -[packages.sdist] -name = "rfc3986-2.0.0.tar.gz" -upload-time = 2022-01-10 00:52:30.832978+00:00 -url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz" -size = 49026 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/2f/c3/da911c8fc22d2664692432570619576b25c7cd0a586401cab08987763094/ty-0.0.89-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl" +upload-time = 2026-10-16 10:34:36+00:00 +size = 14905008 -[packages.sdist.hashes] -sha256 = "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" +[packages.wheels.hashes] +sha256 = "1af774c24117d366b69142d4f35edee518617f8b926d13560d0334cbe5aa3f85" [[packages.wheels]] -name = "rfc3986-2.0.0-py2.py3-none-any.whl" -upload-time = 2022-01-10 00:52:29.594625+00:00 -url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl" -size = 31326 +url = "https://files.pythonhosted.org/packages/5c/c3/aed8d920b9bbf7b582e9fc0305acb2f2c748ede788e7dba9d6952e939c78/ty-0.0.89-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" +upload-time = 2026-10-16 10:34:39+00:00 +size = 15278217 [packages.wheels.hashes] -sha256 = "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd" - -[[packages]] -name = "rfc8785" -version = "0.1.4" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.8" -index = "https://pypi.org/simple/" +sha256 = "a8147811b9b3bc2dc9338cd01c7893ac931fc3fae7aace169047ecbd627e6628" -[packages.sdist] -name = "rfc8785-0.1.4.tar.gz" -upload-time = 2024-09-27 16:33:31.206853+00:00 -url = "https://files.pythonhosted.org/packages/ef/2f/fa1d2e740c490191b572d33dbca5daa180cb423c24396b856f5886371d8b/rfc8785-0.1.4.tar.gz" -size = 14321 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/96/92/c6b4320f08fca44ffe279e5ddeb3eae12869c6341f596931accdefea1182/ty-0.0.89-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl" +upload-time = 2026-10-16 10:34:41+00:00 +size = 15206289 -[packages.sdist.hashes] -sha256 = "e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da" +[packages.wheels.hashes] +sha256 = "6d5f178c91d50fb62461b5e579028b7d2e92872e1d8f02e96ace26464c2277a6" [[packages.wheels]] -name = "rfc8785-0.1.4-py3-none-any.whl" -upload-time = 2024-09-27 16:33:29.683421+00:00 -url = "https://files.pythonhosted.org/packages/4d/78/119878110660b2ad709888c8a1614fce7e2fab39080ab960656dc8605bf6/rfc8785-0.1.4-py3-none-any.whl" -size = 9240 +url = "https://files.pythonhosted.org/packages/3e/61/da22c714fd4f4dff642d60fb0d8bf8da1985d126f21cc89237f8bf707c92/ty-0.0.89-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-16 10:34:44+00:00 +size = 14510395 [packages.wheels.hashes] -sha256 = "520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48" - -[[packages]] -name = "rich" -version = "15.0.0" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.9.0" -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -index = "https://pypi.org/simple/" +sha256 = "95a410f10b26cc3861757b626727db9d979deba7d2a3ab59f964fac8f0514fdc" -[packages.sdist] -name = "rich-15.0.0.tar.gz" -upload-time = 2026-04-12 08:24:00.750018+00:00 -url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz" -size = 230680 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/f0/32/5f9ce2377a77ad9ff36d8996edf86b1f069c1e8716e37c7d6d668402b110/ty-0.0.89-py3-none-manylinux_2_31_riscv64.whl" +upload-time = 2026-10-16 10:34:46+00:00 +size = 14767366 -[packages.sdist.hashes] -sha256 = "edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" +[packages.wheels.hashes] +sha256 = "a70ab1a402746d7837407dc169e461fdf90945bc1eef0d3c0f1be543490060bf" [[packages.wheels]] -name = "rich-15.0.0-py3-none-any.whl" -upload-time = 2026-04-12 08:24:02.830783+00:00 -url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl" -size = 310654 +url = "https://files.pythonhosted.org/packages/63/72/2b28c71d6ebd97b5bb1378f1806598c5dd90278f7b99a431dd3879dddcfc/ty-0.0.89-py3-none-musllinux_1_2_aarch64.whl" +upload-time = 2026-10-16 10:34:48+00:00 +size = 13670284 [packages.wheels.hashes] -sha256 = "33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb" +sha256 = "b55c28a49bd2676eac9b3540a1dcc4cc053e4671c65dcec9fdad3dfe55fb148c" -[[packages]] -name = "securesystemslib" -version = "1.5.1" -marker = "\"release-attestations\" in dependency_groups" -requires-python = "~=3.10" -index = "https://pypi.org/simple/" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/34/cc/68b8ebf98c9bc582fddc3d2b93f6e0f53719d1ddee1c5037dfa2e1cb8467/ty-0.0.89-py3-none-musllinux_1_2_armv7l.whl" +upload-time = 2026-10-16 10:34:50+00:00 +size = 13997891 -[packages.sdist] -name = "securesystemslib-1.5.1.tar.gz" -upload-time = 2026-09-04 06:55:32.066349+00:00 -url = "https://files.pythonhosted.org/packages/81/d4/f9323507144693d869f30f13694035438ab6642153b94e3057945cf0584f/securesystemslib-1.5.1.tar.gz" -size = 947087 +[packages.wheels.hashes] +sha256 = "4c9f22edeb8176f8ab189337e8cbd8f92b66744534a2886df9e252b52ade991c" -[packages.sdist.hashes] -sha256 = "4b8d00abd93707ead10b69eb2b8582376a1364de3b0a71077de534c2ef4985e0" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/cf/4d/a1d61e06092fd5726f48a8711bd32eac6af132308eb222a0027caba4f42d/ty-0.0.89-py3-none-musllinux_1_2_i686.whl" +upload-time = 2026-10-16 10:34:53+00:00 +size = 14326247 + +[packages.wheels.hashes] +sha256 = "0c186185cad57a1469b851096b51345de7b625e353c1f4acd338402daed22fa7" [[packages.wheels]] -name = "securesystemslib-1.5.1-py3-none-any.whl" -upload-time = 2026-09-04 06:55:30.393643+00:00 -url = "https://files.pythonhosted.org/packages/38/fc/c71335a5046d46bf3b761a8c7fa092b40a7721429a5613538f0a28ae80a6/securesystemslib-1.5.1-py3-none-any.whl" -size = 876390 +url = "https://files.pythonhosted.org/packages/f8/d2/466d3762e330504a2c114dbbd0f0383229f9652dccd56277dde008bfe0da/ty-0.0.89-py3-none-musllinux_1_2_x86_64.whl" +upload-time = 2026-10-16 10:34:55+00:00 +size = 14661527 [packages.wheels.hashes] -sha256 = "ada8bdf817da29ece4ba91654f6a162ce7cfbadbc3ae3f840f7313f9d22675de" +sha256 = "5e38302ac218d2404758241093b5542ed0345d4f569096589f1810034244e998" -[[packages]] -name = "setuptools" -version = "84.0.0" -marker = "\"build\" in dependency_groups" -requires-python = ">=3.10" -index = "https://pypi.org/simple/" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b1/d5/dfd7ed5e4af472b20a11ef651da4bca55f9dc8be485cd152337f94caddde/ty-0.0.89-py3-none-win32.whl" +upload-time = 2026-10-16 10:34:57+00:00 +size = 13224078 -[packages.sdist] -name = "setuptools-84.0.0.tar.gz" -upload-time = 2026-08-08 18:27:58.365696+00:00 -url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz" -size = 1168449 +[packages.wheels.hashes] +sha256 = "0146637ed2321717280bfe4e4b20a18813b741da280e8039d7df2bf22b0251f1" -[packages.sdist.hashes] -sha256 = "f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73" +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b4/14/1ee663c422e3a2d910c5cc27e7d84bd7709a3df0aca28a713843c6c3fa5d/ty-0.0.89-py3-none-win_amd64.whl" +upload-time = 2026-10-16 10:35:00+00:00 +size = 13727433 + +[packages.wheels.hashes] +sha256 = "9fc56322a1aff29a19088d2025f3c42a6d25a7035668048bb908c9f8b66f480d" [[packages.wheels]] -name = "setuptools-84.0.0-py3-none-any.whl" -upload-time = 2026-08-08 18:27:56.719708+00:00 -url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl" -size = 818216 +url = "https://files.pythonhosted.org/packages/09/93/af8f7250920823ca6d582bcf0abf803fb92c8b99415ba7f40f2304102c6f/ty-0.0.89-py3-none-win_arm64.whl" +upload-time = 2026-10-16 10:35:02+00:00 +size = 13287101 [packages.wheels.hashes] -sha256 = "51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670" +sha256 = "1d4967c8f7bfd67c9708115b41d7049197c5d20c76fb2e136fe78b0437ada05b" [[packages]] -name = "sigstore" -version = "4.5.0" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "cryptography" }, - { name = "id" }, - { name = "platformdirs" }, - { name = "pyasn1" }, - { name = "pydantic" }, - { name = "pyjwt" }, - { name = "pyopenssl" }, - { name = "requests" }, - { name = "rfc3161-client" }, - { name = "rfc8785" }, - { name = "rich" }, - { name = "sigstore-models" }, - { name = "sigstore-rekor-types" }, - { name = "tuf" }, -] +name = "typing-extensions" +version = "4.16.0" +marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "sigstore-4.5.0.tar.gz" -upload-time = 2026-07-28 07:34:01.717744+00:00 -url = "https://files.pythonhosted.org/packages/18/e0/279419065e2d7102413605b3456122adbbccbc42e010b499c7b882fc01f8/sigstore-4.5.0.tar.gz" -size = 90969 +url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz" +upload-time = 2026-07-02 08:40:05+00:00 +size = 113555 [packages.sdist.hashes] -sha256 = "020d3e07f622b2916bf453e66ff6ff0711e1fdc5ab69e8bd8902f71d9fcb316f" +sha256 = "dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" [[packages.wheels]] -name = "sigstore-4.5.0-py3-none-any.whl" -upload-time = 2026-07-28 07:34:00.211729+00:00 -url = "https://files.pythonhosted.org/packages/bf/7f/51dc313c06dd3ec6c5b308e334492cfdea0b02c9184a0da7b2db8c2a30a1/sigstore-4.5.0-py3-none-any.whl" -size = 111724 +url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl" +upload-time = 2026-07-02 08:40:04+00:00 +size = 45571 [packages.wheels.hashes] -sha256 = "f045b207f2e12605cf775ec38e89c5eda625d71ffa7830477db65e47ec2bc8b2" +sha256 = "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" [[packages]] -name = "sigstore-models" -version = "0.0.6" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "pydantic" }, - { name = "typing-extensions" }, -] +name = "typing-inspection" +version = "0.4.4" +marker = "\"release-attestations\" in dependency_groups or \"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "sigstore_models-0.0.6.tar.gz" -upload-time = 2025-11-26 19:18:12.610187+00:00 -url = "https://files.pythonhosted.org/packages/c6/ed/5c0ff809f90b19f4e971e17c1ed11f4df60082c6010b32a82054087e91e0/sigstore_models-0.0.6.tar.gz" -size = 7037 +url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz" +upload-time = 2026-08-12 12:37:25+00:00 +size = 76928 [packages.sdist.hashes] -sha256 = "c766c09470c2a7e8a4a333c893f07e2001c56a3ff1757b1a246119f53169a849" +sha256 = "547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47" [[packages.wheels]] -name = "sigstore_models-0.0.6-py3-none-any.whl" -upload-time = 2025-11-26 19:18:11.365736+00:00 -url = "https://files.pythonhosted.org/packages/12/dc/7a19864a7bc9f43121ab459e12768ab0080590e3e1b60a29b2f2eb01fb85/sigstore_models-0.0.6-py3-none-any.whl" -size = 13213 +url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl" +upload-time = 2026-08-12 12:37:24+00:00 +size = 14750 [packages.wheels.hashes] -sha256 = "5201a68f4d7d0f8bec1e2f4378eb646b084c52609a4e31db8c385095fff68b2e" +sha256 = "65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147" [[packages]] -name = "sigstore-rekor-types" -version = "0.0.18" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.8" -dependencies = [ - { name = "pydantic" }, -] +name = "urllib3" +version = "2.8.0" +marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "sigstore_rekor_types-0.0.18.tar.gz" -upload-time = 2024-11-22 13:59:54.009830+00:00 -url = "https://files.pythonhosted.org/packages/b4/54/102e772445c5e849b826fbdcd44eb9ad7b3d10fda17b08964658ec7027dc/sigstore_rekor_types-0.0.18.tar.gz" -size = 15687 +url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz" +upload-time = 2026-09-15 19:29:36+00:00 +size = 458972 [packages.sdist.hashes] -sha256 = "19aef25433218ebf9975a1e8b523cc84aaf3cd395ad39a30523b083ea7917ec5" +sha256 = "63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63" [[packages.wheels]] -name = "sigstore_rekor_types-0.0.18-py3-none-any.whl" -upload-time = 2024-11-22 13:59:52.684229+00:00 -url = "https://files.pythonhosted.org/packages/45/7c/f0b4e19fd424df4cc964f5d454e1d814fd2dc3b386342e6040441024318f/sigstore_rekor_types-0.0.18-py3-none-any.whl" -size = 20610 +url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl" +upload-time = 2026-09-15 19:29:34+00:00 +size = 135717 [packages.wheels.hashes] -sha256 = "b62bf38c5b1a62bc0d7fe0ee51a0709e49311d137c7880c329882a8f4b2d1d78" +sha256 = "0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3" [[packages]] -name = "tomli" -version = "2.5.0" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.8" +name = "uv" +version = "0.13.1" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "tomli-2.5.0.tar.gz" -upload-time = 2026-10-07 12:23:37.892554+00:00 -url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz" -size = 17662 +url = "https://files.pythonhosted.org/packages/ef/10/81f499de381ac81e09961e1c19255c5cf49563ac418464def4190b6c7400/uv-0.13.1.tar.gz" +upload-time = 2026-10-14 05:13:35+00:00 +size = 7434239 [packages.sdist.hashes] -sha256 = "264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6" +sha256 = "6df50a86b09be66a413f8f1b16258d585d4dd1cbff591077ae1e6f65c26de2eb" [[packages.wheels]] -name = "tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl" -upload-time = 2026-10-07 12:23:13.941700+00:00 -url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl" -size = 162775 +url = "https://files.pythonhosted.org/packages/c8/2b/79ed9d3a78a43e83ef685bf1e1c84f6c63fe25a0e765b24a169727dd87f1/uv-0.13.1-py3-none-linux_armv6l.whl" +upload-time = 2026-10-14 05:12:50+00:00 +size = 18575444 [packages.wheels.hashes] -sha256 = "a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc" +sha256 = "23dfbdcfd9ff0dc7fefb1b91a6dbca2d492267976becd7c46501f89fc353fdc7" [[packages.wheels]] -name = "tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl" -upload-time = 2026-10-07 12:23:15.215497+00:00 -url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl" -size = 161406 +url = "https://files.pythonhosted.org/packages/db/3d/6ac43d7a15be1d589aae1e15289507f5ccebeac14f28c37882058f2474a3/uv-0.13.1-py3-none-macosx_10_12_x86_64.whl" +upload-time = 2026-10-14 05:12:53+00:00 +size = 17663254 [packages.wheels.hashes] -sha256 = "9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276" +sha256 = "7e60223e3e9636a3734d6c11b3552aeed987c40f1ca2497a546af58f2f883d56" [[packages.wheels]] -name = "tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -upload-time = 2026-10-07 12:23:16.471112+00:00 -url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" -size = 273855 +url = "https://files.pythonhosted.org/packages/3b/31/fad7fd2ac81954d02d33765bf8535eb320fcc2d71f2603a5c1ba0b13b8ef/uv-0.13.1-py3-none-macosx_11_0_arm64.whl" +upload-time = 2026-10-14 05:12:56+00:00 +size = 14391237 [packages.wheels.hashes] -sha256 = "752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52" +sha256 = "cfa719fbbd23f26713394dea2692b7f146aa1b5713ce5e776e878701214ea52c" [[packages.wheels]] -name = "tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" -upload-time = 2026-10-07 12:23:18.166140+00:00 -url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" -size = 284910 +url = "https://files.pythonhosted.org/packages/9f/6a/ff4c51a245345a167eed090e8cea27d5979e8b665c97617805bb8748966f/uv-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl" +upload-time = 2026-10-14 05:12:58+00:00 +size = 18047204 [packages.wheels.hashes] -sha256 = "c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7" +sha256 = "f2518450da2f6d0ea8a943c9f402abda63d5a66acc9999216c2792115b922ba4" [[packages.wheels]] -name = "tomli-2.5.0-cp315-cp315-win_amd64.whl" -upload-time = 2026-10-07 12:23:23.098344+00:00 -url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl" -size = 114589 +url = "https://files.pythonhosted.org/packages/5f/04/268c9c17d9a808f41b71241f9d61030c4f262213815eeaa329bae1c76c3f/uv-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl" +upload-time = 2026-10-14 05:13:00+00:00 +size = 18122326 [packages.wheels.hashes] -sha256 = "d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5" +sha256 = "29b9d0dbe9c44d3dcea8af9e7b4ea2444c7a0c9ec0a11e2f2d357fb3afa2c05b" [[packages.wheels]] -name = "tomli-2.5.0-cp315-cp315-win_arm64.whl" -upload-time = 2026-10-07 12:23:24.233048+00:00 -url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl" -size = 111493 +url = "https://files.pythonhosted.org/packages/44/88/33b72d5c4aed94a504fe92b7312f76c54e16a00b695045053f7a72953815/uv-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" +upload-time = 2026-10-14 05:13:03+00:00 +size = 18139075 [packages.wheels.hashes] -sha256 = "75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd" +sha256 = "7161cfb854f7353fd351e5e735e3659ed48412352b1edd4d93212c82ec0cbdf0" [[packages.wheels]] -name = "tomli-2.5.0-py3-none-any.whl" -upload-time = 2026-10-07 12:23:36.875825+00:00 -url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl" -size = 14765 +url = "https://files.pythonhosted.org/packages/54/59/866ca1f07f899f007fcc006663d341157c5dbdae7271e5004c5a120022ee/uv-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl" +upload-time = 2026-10-14 05:13:05+00:00 +size = 18840481 [packages.wheels.hashes] -sha256 = "32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b" - -[[packages]] -name = "tomli-w" -version = "1.2.0" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.9" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "tomli_w-1.2.0.tar.gz" -upload-time = 2025-01-15 12:07:24.262974+00:00 -url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz" -size = 7184 - -[packages.sdist.hashes] -sha256 = "2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021" +sha256 = "d6aaec25ff6900ee30a29a2e7a3335aff19a2305c60488bb4e0f29cf1d6ada9f" [[packages.wheels]] -name = "tomli_w-1.2.0-py3-none-any.whl" -upload-time = 2025-01-15 12:07:22.074224+00:00 -url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl" -size = 6675 +url = "https://files.pythonhosted.org/packages/dd/a9/928d7435a00d1171f4557212704d71d709cf4b6024f2ee6b0f582251c761/uv-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" +upload-time = 2026-10-14 05:13:07+00:00 +size = 20106013 [packages.wheels.hashes] -sha256 = "188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90" - -[[packages]] -name = "tomlkit" -version = "0.15.2" -marker = "\"lock-tools\" in dependency_groups or \"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups" -requires-python = ">=3.9" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "tomlkit-0.15.2.tar.gz" -upload-time = 2026-10-16 04:07:08.166827+00:00 -url = "https://files.pythonhosted.org/packages/08/72/fc01563df95c189a9f65850096bdc205bc2fe37da6c740cd64afaf18d07e/tomlkit-0.15.2.tar.gz" -size = 190522 - -[packages.sdist.hashes] -sha256 = "83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6" +sha256 = "81345a4733737a378db988aea48fd5d1c76c092cf231e6ddfdb6db225b4b21e5" [[packages.wheels]] -name = "tomlkit-0.15.2-py3-none-any.whl" -upload-time = 2026-10-16 04:07:09.216014+00:00 -url = "https://files.pythonhosted.org/packages/da/46/7d84138a60a8bd796825b2d38ebe92d0d16501ca57d501b7c5d677401bbc/tomlkit-0.15.2-py3-none-any.whl" -size = 52225 +url = "https://files.pythonhosted.org/packages/3c/70/ac84eee888c4a11e12c361c10fdf3f726ef6f8f1178302c80a9dee5189a3/uv-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl" +upload-time = 2026-10-14 05:13:10+00:00 +size = 19840034 [packages.wheels.hashes] -sha256 = "05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437" - -[[packages]] -name = "towncrier" -version = "26.9.0" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.9" -dependencies = [ - { name = "click" }, - { name = "jinja2" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "towncrier-26.9.0.tar.gz" -upload-time = 2026-09-04 12:57:25.341701+00:00 -url = "https://files.pythonhosted.org/packages/92/d4/e85ce614bf45b1a7fe13b82cc687819c2eaac5a1dd94b84834322095fd98/towncrier-26.9.0.tar.gz" -size = 79338 - -[packages.sdist.hashes] -sha256 = "ace9031631c718cc0709107b8755e6e31350eb8acdc877af43df3177addb9a55" +sha256 = "0f9991b8eedca00b6aeb9a8fc6b225f6afa2e24ec9de6b6ef72845f3a545639c" [[packages.wheels]] -name = "towncrier-26.9.0-py3-none-any.whl" -upload-time = 2026-09-04 12:57:23.986160+00:00 -url = "https://files.pythonhosted.org/packages/82/88/f9340b545dafa64de053798e760515a2736fbb60092cf9aa6b146ccf3ede/towncrier-26.9.0-py3-none-any.whl" -size = 67030 +url = "https://files.pythonhosted.org/packages/14/0b/6602a613533602e7a083e908c4375f9b0e78a94ba2e21c29b21c41a0a611/uv-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-14 05:13:13+00:00 +size = 17279385 [packages.wheels.hashes] -sha256 = "ae4d223e6aadaff98d29b7f09e58d9c9ccf4cd3b455a2d2e0486d432cd8bf660" - -[[packages]] -name = "truststore" -version = "0.10.5" -marker = "\"lock-tools\" in dependency_groups" -requires-python = ">=3.10" -index = "https://pypi.org/simple/" +sha256 = "696dcf6d807d85da978f4db8472da81dccc1254b18b52cbd1367c4d923c421e0" -[packages.sdist] -name = "truststore-0.10.5.tar.gz" -upload-time = 2026-10-12 22:27:31.808128+00:00 -url = "https://files.pythonhosted.org/packages/ee/9f/c5201d42a484c061e528825fc8e2d565f5abd50a4ced6fb7d29c4ec99b2b/truststore-0.10.5.tar.gz" -size = 28091 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/09/f6/52e2136653a1f3a4dc489da83444f062ed588570cc3e5242fda50996951a/uv-0.13.1-py3-none-manylinux_2_28_aarch64.whl" +upload-time = 2026-10-14 05:13:15+00:00 +size = 16577060 -[packages.sdist.hashes] -sha256 = "30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd" +[packages.wheels.hashes] +sha256 = "ccbe233d4bce39865af7c30ec1fb28b0cc353f9d51711629f54fca71b8db7382" [[packages.wheels]] -name = "truststore-0.10.5-py3-none-any.whl" -upload-time = 2026-10-12 22:27:30.377491+00:00 -url = "https://files.pythonhosted.org/packages/51/e9/3a7820be2bb0fe53b6bc9c3be26d3d1158004e4c3ab953aa6840b955b1e9/truststore-0.10.5-py3-none-any.whl" -size = 19017 +url = "https://files.pythonhosted.org/packages/df/7c/a2f4ff386673a4bed33be69fdb857341eaa6b9f31eed3005ffc15df94d7f/uv-0.13.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl" +upload-time = 2026-10-14 05:13:18+00:00 +size = 18417071 [packages.wheels.hashes] -sha256 = "9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c" - -[[packages]] -name = "tuf" -version = "7.0.1" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "securesystemslib" }, - { name = "urllib3" }, -] -index = "https://pypi.org/simple/" +sha256 = "ec1bdc90e2cccc32f51dcea2fa4ec9082b321c77f3fe51a0407d40e8a44f540f" -[packages.sdist] -name = "tuf-7.0.1.tar.gz" -upload-time = 2026-09-02 06:19:52.071733+00:00 -url = "https://files.pythonhosted.org/packages/9c/dd/52e7390cbac308e6b1cfe6be9bc3a96fe26326d5893cc297a52bb72792a9/tuf-7.0.1.tar.gz" -size = 274148 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/4a/6e/a2d375f8fcb453e75caf6d1503f7e149e76a3403091a2a77ada4d42482c1/uv-0.13.1-py3-none-manylinux_2_31_riscv64.whl" +upload-time = 2026-10-14 05:13:20+00:00 +size = 18329037 -[packages.sdist.hashes] -sha256 = "5ada1db78da3518fa851e28588f4b2249b8662a28ae528f54afa2a1de541fb31" +[packages.wheels.hashes] +sha256 = "b5e1b930cbb463c0d8f6b925743ddbae31277841f16c324d14e1a815b0c3b824" [[packages.wheels]] -name = "tuf-7.0.1-py3-none-any.whl" -upload-time = 2026-09-02 06:19:50.608554+00:00 -url = "https://files.pythonhosted.org/packages/a2/10/171e306e4a19ff980b9029ce5a523881e34bc6a2a52750fb1853f49f0fb9/tuf-7.0.1-py3-none-any.whl" -size = 56253 +url = "https://files.pythonhosted.org/packages/0c/51/070341c84c141e73933f6087dcc9a5fd276cb034bdc0316f2d2dffd0cd44/uv-0.13.1-py3-none-musllinux_1_1_i686.whl" +upload-time = 2026-10-14 05:13:23+00:00 +size = 18130710 [packages.wheels.hashes] -sha256 = "d30434bda6e079ab303fb30d1b3006d939a10ca34783b1573d61cd9b802fa45c" - -[[packages]] -name = "typing-extensions" -version = "4.16.0" -marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups" -requires-python = ">=3.9" -index = "https://pypi.org/simple/" +sha256 = "4af921c96f5f85c456c7fa93a9bc13514a2a5bbab11ce63dbf490d6e5bfef503" -[packages.sdist] -name = "typing_extensions-4.16.0.tar.gz" -upload-time = 2026-07-02 08:40:05.920538+00:00 -url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz" -size = 113555 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b9/0d/529a302582685e77e5e2d16de554e7c230b45c6175b0b0f234af9795d981/uv-0.13.1-py3-none-musllinux_1_1_x86_64.whl" +upload-time = 2026-10-14 05:13:25+00:00 +size = 19307551 -[packages.sdist.hashes] -sha256 = "dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" +[packages.wheels.hashes] +sha256 = "6f1f391756385e2cd1521fe4a198e0ca764ef498dd677cb214b95a69a7ce3608" [[packages.wheels]] -name = "typing_extensions-4.16.0-py3-none-any.whl" -upload-time = 2026-07-02 08:40:04.659120+00:00 -url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl" -size = 45571 +url = "https://files.pythonhosted.org/packages/7f/7f/c81ec77c0eb772ec4fc515e733625969c64df55ee2f7b6b3fb1e07c2ee26/uv-0.13.1-py3-none-win32.whl" +upload-time = 2026-10-14 05:13:28+00:00 +size = 16434446 [packages.wheels.hashes] -sha256 = "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" - -[[packages]] -name = "typing-inspection" -version = "0.4.4" -marker = "\"release-attestations\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "typing-extensions" }, -] -index = "https://pypi.org/simple/" +sha256 = "1f6ae6f843ee01a1bb69e2e7a881a452890eb9acc686a8c37e8e95093a58f68e" -[packages.sdist] -name = "typing_inspection-0.4.4.tar.gz" -upload-time = 2026-08-12 12:37:25.997329+00:00 -url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz" -size = 76928 +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/2a/8e/d23278bb327d2a75dc1d033c81b7fe23bbb28e363e02e7f306abd047924a/uv-0.13.1-py3-none-win_amd64.whl" +upload-time = 2026-10-14 05:13:30+00:00 +size = 15404994 -[packages.sdist.hashes] -sha256 = "547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47" +[packages.wheels.hashes] +sha256 = "4f693cd4feb52f08f30474c4612f24be2050472609be65b9960b635f286da284" [[packages.wheels]] -name = "typing_inspection-0.4.4-py3-none-any.whl" -upload-time = 2026-08-12 12:37:24.648475+00:00 -url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl" -size = 14750 +url = "https://files.pythonhosted.org/packages/0a/e0/f27f161ee2ad242cb714fe7185f63cc2a88b83c4d8b375ca8c580f9f23b7/uv-0.13.1-py3-none-win_arm64.whl" +upload-time = 2026-10-14 05:13:32+00:00 +size = 15899284 [packages.wheels.hashes] -sha256 = "65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147" +sha256 = "b173413a5dc18437b2d28266a02be636f35ac7e97741773aed163ed2143abfaf" [[packages]] -name = "urllib3" -version = "2.8.0" -marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups" -requires-python = ">=3.10" +name = "virtualenv" +version = "21.14.8" +marker = "\"test\" in dependency_groups" index = "https://pypi.org/simple/" [packages.sdist] -name = "urllib3-2.8.0.tar.gz" -upload-time = 2026-09-15 19:29:36.253420+00:00 -url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz" -size = 458972 +url = "https://files.pythonhosted.org/packages/0b/6a/fbdeea5c07d5674139639d42a97a41d26e9b9b8cb2a1265fa936f655ee20/virtualenv-21.14.8.tar.gz" +upload-time = 2026-10-16 00:09:09+00:00 +size = 5474561 [packages.sdist.hashes] -sha256 = "63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63" +sha256 = "394e45ed610ceffb5018e1a179e3a8012c02fd87fe792ab4aae67ce5e7e3afbd" [[packages.wheels]] -name = "urllib3-2.8.0-py3-none-any.whl" -upload-time = 2026-09-15 19:29:34.577402+00:00 -url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl" -size = 135717 +url = "https://files.pythonhosted.org/packages/14/01/cd63b97dd556293e1c74d76053936d9cef11a28eac605c735ae9f0ee324f/virtualenv-21.14.8-py3-none-any.whl" +upload-time = 2026-10-16 00:09:07+00:00 +size = 5487606 [packages.wheels.hashes] -sha256 = "0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3" +sha256 = "d2a347fdbc1a65e33c2c5e84dc767b4a3538a25749536a752744d4fe1f71c37a" [[packages]] name = "wcwidth" version = "0.9.2" marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" -requires-python = ">=3.9" index = "https://pypi.org/simple/" [packages.sdist] -name = "wcwidth-0.9.2.tar.gz" -upload-time = 2026-10-05 00:24:05.521972+00:00 url = "https://files.pythonhosted.org/packages/f0/b4/7830542634bb2d3e62aa3b586a72d5b3b6c91c3168929e7000ef3fed041d/wcwidth-0.9.2.tar.gz" +upload-time = 2026-10-05 00:24:05+00:00 size = 955039 [packages.sdist.hashes] sha256 = "ae0ef90b90f6af38b54f1fe6d58662ec33b3cb4b8391958a62416d654231727b" [[packages.wheels]] -name = "wcwidth-0.9.2-cp310-abi3-macosx_10_9_x86_64.whl" -upload-time = 2026-10-05 00:23:22.649242+00:00 url = "https://files.pythonhosted.org/packages/59/1e/4532a81fb9dfbf4114a816775e0a36c3a64ee1d1f4bba2094e2da50be5dc/wcwidth-0.9.2-cp310-abi3-macosx_10_9_x86_64.whl" +upload-time = 2026-10-05 00:23:22+00:00 size = 605343 [packages.wheels.hashes] sha256 = "7ef5a940bd5e30bac6e721f1a48fce0cd7bb3ece19e9c5d139e72c76c35cfd07" [[packages.wheels]] -name = "wcwidth-0.9.2-cp310-abi3-macosx_11_0_arm64.whl" -upload-time = 2026-10-05 00:23:27.049292+00:00 url = "https://files.pythonhosted.org/packages/a0/07/cb6940e81134b7ed25fa312ee9ab536a63db0793b149f88a90e603ceace9/wcwidth-0.9.2-cp310-abi3-macosx_11_0_arm64.whl" +upload-time = 2026-10-05 00:23:27+00:00 size = 609960 [packages.wheels.hashes] sha256 = "ae0800c5339423cc53d33a266ad264b42ba8aaa16d4464f6e6b1bee607f50b17" [[packages.wheels]] -name = "wcwidth-0.9.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" -upload-time = 2026-10-05 00:23:32.448318+00:00 +url = "https://files.pythonhosted.org/packages/a4/80/15ad05d40bfa99155639fb9e13b3d77083aa0fab893c816db2543d29005c/wcwidth-0.9.2-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl" +upload-time = 2026-10-05 00:23:38+00:00 +size = 762566 + +[packages.wheels.hashes] +sha256 = "9e542f1f8475b78452a295495d7a5bc3ead565112e9446a64dc93462a41c2a79" + +[[packages.wheels]] url = "https://files.pythonhosted.org/packages/bc/f0/b8ef7758003d66b60f093695831a86dcc726aac01ee6446ffcbda27b61e3/wcwidth-0.9.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" +upload-time = 2026-10-05 00:23:32+00:00 size = 771632 [packages.wheels.hashes] sha256 = "674b518af28d38ee645ff97b74f5760abee5fad4bac74413bfc4b881ef2ce724" [[packages.wheels]] -name = "wcwidth-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" -upload-time = 2026-10-05 00:23:40.175985+00:00 url = "https://files.pythonhosted.org/packages/db/6c/f940133c71427c208575910e981942bd78c98b1f7cd0d1425ca4b7457c04/wcwidth-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-10-05 00:23:40+00:00 size = 769763 [packages.wheels.hashes] sha256 = "751bef0ab404b6a1dc028b56b4b85d46486be1c55833f80da533e42dc691f389" [[packages.wheels]] -name = "wcwidth-0.9.2-cp310-abi3-win_amd64.whl" -upload-time = 2026-10-05 00:23:57.489330+00:00 +url = "https://files.pythonhosted.org/packages/92/8f/285f862826f721964ec7c42f81dc53d23afbd723a0f4cd989651f8218e25/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl" +upload-time = 2026-10-05 00:23:33+00:00 +size = 782134 + +[packages.wheels.hashes] +sha256 = "c3d80f39ba4653a595edae9aa46a509d14883790a8fc23c5db221ceb207f64b7" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/c2/2d/64aa54882a5d556d3654c1f926d9118b797461033e23a158409941a37c8f/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_i686.whl" +upload-time = 2026-10-05 00:23:41+00:00 +size = 782851 + +[packages.wheels.hashes] +sha256 = "0a47e03d8293590ecce66c45dc20ff7b4b885e3c78093722239585eca0d77ab2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/59/39/52389f6de7fe2e9c14ceb8253dd99034bd86e1c87847ea3c100a97dded9a/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl" +upload-time = 2026-10-05 00:23:43+00:00 +size = 784621 + +[packages.wheels.hashes] +sha256 = "67d901a4ad99249eb775b4ee4769ca97fa405d35a75f46e83166910a47003f04" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b3/8b/20225500a076ace27bbcc8a6fd7c55125133c57a618816c7b7b8b73070b1/wcwidth-0.9.2-cp310-abi3-win32.whl" +upload-time = 2026-10-05 00:23:55+00:00 +size = 593123 + +[packages.wheels.hashes] +sha256 = "ee1fd0db9d9fd711a70f3e7765e0e04c05d26982fa05361456163062549d7da4" + +[[packages.wheels]] url = "https://files.pythonhosted.org/packages/5a/d6/b0690f55ea0483530a18bac917fbadbf54f35122510446fc370f5f1c2453/wcwidth-0.9.2-cp310-abi3-win_amd64.whl" +upload-time = 2026-10-05 00:23:57+00:00 size = 597827 [packages.wheels.hashes] sha256 = "2a9746de704242bd4fdaabb31dd46b82f694a56a8d21081ad89b679a89da9fec" [[packages.wheels]] -name = "wcwidth-0.9.2-cp310-abi3-win_arm64.whl" -upload-time = 2026-10-05 00:23:51.517153+00:00 url = "https://files.pythonhosted.org/packages/e5/11/6ecf4e9e268ab1a4ec617ffcccc2ee4a71301625f5490912dbaba462fa9c/wcwidth-0.9.2-cp310-abi3-win_arm64.whl" +upload-time = 2026-10-05 00:23:51+00:00 size = 598415 [packages.wheels.hashes] sha256 = "b9c6ab615e03723b7f8760ea2f27758d656e7e13b51515c9dca5c3e8b04612fa" [[packages.wheels]] -name = "wcwidth-0.9.2-py3-none-any.whl" -upload-time = 2026-10-05 00:23:21.097514+00:00 +url = "https://files.pythonhosted.org/packages/82/8a/a28d61d910005ac93dfe48be3a0ebaa49352d88cebd25323e69e6ff2f4a8/wcwidth-0.9.2-cp315-cp315t-macosx_10_15_x86_64.whl" +upload-time = 2026-10-05 00:23:25+00:00 +size = 606575 + +[packages.wheels.hashes] +sha256 = "42dbcb76ce8af39e2c9db410ac3f9bdf4e47eb41d6f44525952f172d3d98f724" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/01/c2/a3c66bd32766c8f4d6dc47d572532ba014fe5be30489f2576aff7cada363/wcwidth-0.9.2-cp315-cp315t-macosx_11_0_arm64.whl" +upload-time = 2026-10-05 00:23:30+00:00 +size = 611045 + +[packages.wheels.hashes] +sha256 = "138e1f8898e431b2f2d7881f8ca8d75591c1d3c21aa53f54e989bd6b39811da2" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ec/8a/d39964f8f8c019d7d439b9b501d3e7bb42fee69f00354040ba0b27b5824c/wcwidth-0.9.2-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl" +upload-time = 2026-10-05 00:23:47+00:00 +size = 778143 + +[packages.wheels.hashes] +sha256 = "5175609bf8cc7398a5f48aa35207bd64ebf9f45e4c70df65f7fdc7a988041a3c" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/2f/53/525da13e8f9ff7b5b4e74ec6f8d68bdee63905796972e086c6b1b96670d2/wcwidth-0.9.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" +upload-time = 2026-10-05 00:23:36+00:00 +size = 786630 + +[packages.wheels.hashes] +sha256 = "e5f669ae8c3d969c72032f9cdee019674b666e522d45e1e2099a2e9dda4a341d" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/ef/9f/d6a0c6df354b9d93466548a65cbf4ffcb48c719bbd307504cf3e76740837/wcwidth-0.9.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-10-05 00:23:49+00:00 +size = 784839 + +[packages.wheels.hashes] +sha256 = "196b47cf32f9df27ccda6dc513237f3c2429c4c659db428d60a5bc443d10f270" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/bf/d7/3021feed1ed7926021ec134943ad3b24a2f7ea742cc9976461171482ed77/wcwidth-0.9.2-cp315-cp315t-win32.whl" +upload-time = 2026-10-05 00:24:02+00:00 +size = 600845 + +[packages.wheels.hashes] +sha256 = "0cd4f7f2e53905dcb110d213a4c8529b6733fa3d232d8c717f946cc69a10349b" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/63/80/6a03356d8ee38261e3a78cf89ee03d8e7f12c572d969237be00869e2dc73/wcwidth-0.9.2-cp315-cp315t-win_amd64.whl" +upload-time = 2026-10-05 00:24:04+00:00 +size = 605441 + +[packages.wheels.hashes] +sha256 = "33df042f96c61ed3cd5fb3742fba427553a635bc578799857a48aa79f774a0b9" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/0c/48/1a308a86a833fd12ff7a08d0d2491ff4a72c8a92d12f5ead8317630f771e/wcwidth-0.9.2-cp315-cp315t-win_arm64.whl" +upload-time = 2026-10-05 00:23:54+00:00 +size = 605885 + +[packages.wheels.hashes] +sha256 = "48719a9bc76c2f84238693fe5013571fa5beffa3621cf228f1f3a9e30dae84b8" + +[[packages.wheels]] url = "https://files.pythonhosted.org/packages/9c/b4/0bfa065af506540d9d558e3e5548cff00bc1f9b24e6e2a8512498e8628de/wcwidth-0.9.2-py3-none-any.whl" +upload-time = 2026-10-05 00:23:21+00:00 size = 301667 [packages.wheels.hashes] sha256 = "89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e" +[[packages]] +name = "werkzeug" +version = "3.1.9" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/a4/34/4dd12fc8bb7d61c91467ec3efe415ffa7d5456f799954b40c5bbaeae470e/werkzeug-3.1.9.tar.gz" +upload-time = 2026-09-27 18:33:41+00:00 +size = 940188 + +[packages.sdist.hashes] +sha256 = "55ca7c70a75689be937aa27f8ff4b018f06ff4838fc73045560bf0f5a1291060" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/a1/38/df03f564f43cec2684823f3cccae1a652ee7face1cbaa76fb223096e64d7/werkzeug-3.1.9-py3-none-any.whl" +upload-time = 2026-09-27 18:33:39+00:00 +size = 228700 + +[packages.wheels.hashes] +sha256 = "6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab" + +[[packages]] +name = "wheel-filename" +version = "1.4.2" +marker = "\"test\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +url = "https://files.pythonhosted.org/packages/38/be/726dab762b770d0417e505c58e26d661aac1ec0c831e483cda4817ca2417/wheel_filename-1.4.2.tar.gz" +upload-time = 2024-12-01 13:03:16+00:00 +size = 7911 + +[packages.sdist.hashes] +sha256 = "87891c465dcbb40b40394a906f01a93214bdd51aa5d25e3a9a59cae62bc298fd" + +[[packages.wheels]] +url = "https://files.pythonhosted.org/packages/b4/0f/6e97a3bc38cdde32e3ec49f8c0903fe3559ec9ec9db181782f0bb4417717/wheel_filename-1.4.2-py3-none-any.whl" +upload-time = 2024-12-01 13:03:00+00:00 +size = 6195 + +[packages.wheels.hashes] +sha256 = "3fa599046443d4ca830d06e3d180cd0a675d5871af0a68daa5623318bb4d17e3" + [[packages]] name = "xmltodict" version = "1.0.4" marker = "\"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups" -requires-python = ">=3.9" index = "https://pypi.org/simple/" [packages.sdist] -name = "xmltodict-1.0.4.tar.gz" -upload-time = 2026-02-22 02:21:22.074263+00:00 url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz" +upload-time = 2026-02-22 02:21:22+00:00 size = 26124 [packages.sdist.hashes] sha256 = "6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61" [[packages.wheels]] -name = "xmltodict-1.0.4-py3-none-any.whl" -upload-time = 2026-02-22 02:21:21.039836+00:00 url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl" +upload-time = 2026-02-22 02:21:21+00:00 size = 13580 [packages.wheels.hashes] @@ -2456,66 +4826,26 @@ sha256 = "a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a" name = "yq" version = "4.4.5" marker = "\"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups" -requires-python = ">=3.8" -dependencies = [ - { name = "argcomplete" }, - { name = "pyyaml" }, - { name = "tomlkit" }, - { name = "xmltodict" }, -] index = "https://pypi.org/simple/" [packages.sdist] -name = "yq-4.4.5.tar.gz" -upload-time = 2026-10-11 02:20:56.748595+00:00 url = "https://files.pythonhosted.org/packages/fd/be/ae37199d28a0a99eb79084f91da94f6e7e58550552361a2397c5032d5d37/yq-4.4.5.tar.gz" +upload-time = 2026-10-11 02:20:56+00:00 size = 54535 [packages.sdist.hashes] sha256 = "e17b21a927d04ae33f61a7284351b69b0a539bfb73b852a04a907b79bcd15de2" [[packages.wheels]] -name = "yq-4.4.5-py3-none-any.whl" -upload-time = 2026-10-11 02:20:55.272902+00:00 url = "https://files.pythonhosted.org/packages/fd/38/cca2b04f284c788ded19a9401e3049fd1f82d0467c1d6bd361915a77c063/yq-4.4.5-py3-none-any.whl" +upload-time = 2026-10-11 02:20:55+00:00 size = 30255 [packages.wheels.hashes] sha256 = "4e8b4e54979b26554c72a7e669009017a7fdd3fb00bbae0b26f82a86636c289f" +[tool.release-creator] +created-at = 2026-10-24 09:10:11.953943+00:00 + [tool.nab] -nab-version = "0.0.22" -created-at = 2026-10-24 06:08:31.473006+00:00 -command-line = [ - "nab", - "-vv", - "lock", - "--no-cache", - "--groups", - "build", - "lock-tools", - "lock-reader", - "release-notes", - "release-news", - "release-prepare", - "release-validate", - "release-check", - "release-attestations", - "release-shell", - "release-hooks", - "release-hook-check", - "--output", - "pylock.toml", - "--upgrade", -] -input-path = "pyproject.toml" -mode = "universal" -python-specifier = ">=3.15,<3.16" -platforms = [ - "linux_x86_64", - "linux_aarch64", - "macos_x86_64", - "macos_arm64", - "windows_amd64", - "windows_arm64", -] +created-at = 2026-10-24 09:10:11.953943+00:00 diff --git a/pyproject.toml b/pyproject.toml index 3507510..950aad5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,14 +11,14 @@ license-files = [ "LICENSE" ] build = [ "Cython>=3.0", "setuptools>=80" ] lock-tools = [ "nab==0.0.22", "tomlkit>=0.13" ] lock-reader = [ "certifi>=2026.7.22", "packaging>=26.3" ] -release-notes = [ "release-creator>=1.0.0" ] -release-news = [ "release-creator>=1.0.0", "towncrier>=26.9" ] -release-prepare = [ "release-creator>=1.0.0", "towncrier>=26.9", "prek>=0.2" ] -release-validate = [ "release-creator>=1.0.0", "prek>=0.2" ] +release-notes = [ "release-creator>=1.1.0" ] +release-news = [ "release-creator>=1.1.0", "towncrier>=26.9" ] +release-prepare = [ "release-creator>=1.1.0", "towncrier>=26.9", "prek>=0.2" ] +release-validate = [ "release-creator>=1.1.0", "prek>=0.2" ] release-check = [ "prek>=0.2" ] release-attestations = [ "certifi>=2026.7.22", "pypi-attestations>=0.0.30" ] release-shell = [ "prek>=0.2", "yq>=4.4.5" ] -release-hooks = [ "release-creator>=1.0.0", "prek>=0.2" ] +release-hooks = [ "release-creator>=1.1.0", "prek>=0.2" ] release-hook-check = [ "prek>=0.2", "yq>=4.4.5" ] test = [ "certifi", @@ -27,7 +27,7 @@ test = [ "hatchling>=1.28", "pytest>=9", "pytest-httpserver", - "pytest-mock>=3.17", + "pytest-mock>=3.16", "pytest-xdist>=3", "pyyaml>=6", "tomlkit>=0.13", @@ -68,7 +68,16 @@ uploaded-prior-to = "2026-10-18T05:45:09Z" uploaded-prior-to = "2026-10-18T05:45:09Z" [tool.nab.packages.release-creator] -uploaded-prior-to = "2026-10-24T06:07:46Z" +uploaded-prior-to = "2026-10-24T09:08:40Z" + +[tool.release-creator.locks.exclude-newer-package] +nab = "2026-10-18T05:45:09Z" +nab-index = "2026-10-18T05:45:09Z" +nab-markersets = "2026-10-18T05:45:09Z" +nab-project = "2026-10-18T05:45:09Z" +nab-provider = "2026-10-18T05:45:09Z" +nab-resolver = "2026-10-18T05:45:09Z" +release-creator = "2026-10-24T09:08:40Z" [tool.ruff] line-length = 120 diff --git a/recheck-hook-update-cooldown/action.yml b/recheck-hook-update-cooldown/action.yml index c6ed2ec..f60f3c2 100644 --- a/recheck-hook-update-cooldown/action.yml +++ b/recheck-hook-update-cooldown/action.yml @@ -1,5 +1,5 @@ name: Recheck cooled hook updates -description: Recheck failed bot hook-update cooldown jobs each day +description: Recheck failed hook-update cooldown jobs each day inputs: workflow: description: Workflow file containing the hook cooldown job @@ -18,9 +18,8 @@ runs: set -euo pipefail gh api --paginate "repos/$GITHUB_REPOSITORY/pulls?state=open&per_page=100" | jq -c --arg repository "$GITHUB_REPOSITORY" ' - .[] | select(.user.type == "Bot" and - .head.repo.full_name == $repository and .base.repo.full_name == $repository) | - {number, head: .head.sha, branch: .head.ref} + .[] | select(.head.repo.full_name != null and .base.repo.full_name == $repository) | + {number, head: .head.sha, branch: .head.ref, headRepository: .head.repo.full_name} ' >"$RUNNER_TEMP/hook-pulls.jsonl" - name: Rerun the failed hook checks shell: bash @@ -40,9 +39,10 @@ runs: fi head=$(jq -r .head <<<"$pull") branch=$(jq -r .branch <<<"$pull") + head_repository=$(jq -r .headRepository <<<"$pull") runs=$(gh api --paginate -X GET "repos/$GITHUB_REPOSITORY/actions/workflows/$WORKFLOW/runs" \ -f event=pull_request -f head_sha="$head" -f per_page=100 | - jq -sc --arg repository "$GITHUB_REPOSITORY" --arg branch "$branch" --arg head "$head" \ + jq -sc --arg repository "$head_repository" --arg branch "$branch" --arg head "$head" \ --argjson number "$number" ' [.[].workflow_runs[] | select( .event == "pull_request" and .head_repository.full_name == $repository and diff --git a/refresh-release-lock/action.yml b/refresh-release-lock/action.yml index 44eb2e4..352499d 100644 --- a/refresh-release-lock/action.yml +++ b/refresh-release-lock/action.yml @@ -1,9 +1,9 @@ name: Refresh a release-tool lock -description: Refresh or check the Nab lock for selected release groups +description: Refresh or check a full project lock and optional Nab matrix lock inputs: lock-groups: - description: Space-separated dependency groups recorded in pylock.toml - required: true + description: Space-separated dependency groups for legacy Nab locks + default: "" additional-lock: description: Optional secondary pylock basename for a separate build matrix default: "" @@ -32,32 +32,51 @@ outputs: runs: using: composite steps: + - id: producer + name: Identify lock producer + shell: bash + env: + ADDITIONAL_LOCK: ${{ inputs.additional-lock }} + ADDITIONAL_GROUPS: ${{ inputs.additional-lock-groups }} + ADDITIONAL_SELECTION: ${{ inputs.additional-lock-selection }} + MATRIX_PYTHON: ${{ inputs.additional-matrix-python }} + MATRIX_PLATFORMS: ${{ inputs.additional-matrix-platforms }} + MATRIX_IMPLEMENTATIONS: ${{ inputs.additional-matrix-implementations }} + run: | + set -euo pipefail + if [[ -z "$ADDITIONAL_LOCK" && + ("$ADDITIONAL_SELECTION" != build-or-groups || + -n "$ADDITIONAL_GROUPS$MATRIX_PYTHON$MATRIX_PLATFORMS$MATRIX_IMPLEMENTATIONS") ]]; then + echo '::error::secondary matrix inputs require additional-lock' >&2 + exit 1 + fi + provider=$(python3 "$GITHUB_ACTION_PATH/../tools/lock_groups.py" pylock.toml --provider) + echo "name=$provider" >>"$GITHUB_OUTPUT" - uses: $/install-locked-tools with: groups: lock-tools lock-source: shared venv: ${{ runner.temp }}/lock-tools + - uses: $/install-locked-tools + if: steps.producer.outputs.name == 'release-creator' + with: + groups: release-notes + lock-source: shared + venv: ${{ runner.temp }}/project-lock-tools - name: Check recent package cutoffs shell: bash - run: uv run --no-project --python "$RUNNER_TEMP/lock-tools" python "$GITHUB_ACTION_PATH/../tools/check_recent_cutoffs.py" pyproject.toml + run: python3 "$GITHUB_ACTION_PATH/../tools/check_recent_cutoffs.py" pyproject.toml - name: Expire reviewed package cutoffs if: inputs.mode == 'refresh' shell: bash - run: | - set -euo pipefail - uv run --no-project --python "$RUNNER_TEMP/lock-tools" python \ - "$GITHUB_ACTION_PATH/../tools/expire_cutoffs.py" pyproject.toml - - name: Resolve release groups + run: >- + uv run --no-project --python "$RUNNER_TEMP/lock-tools" python "$GITHUB_ACTION_PATH/../tools/expire_cutoffs.py" pyproject.toml + - name: Resolve selected Nab groups + if: steps.producer.outputs.name == 'nab' shell: bash env: LOCK_GROUPS: ${{ inputs.lock-groups }} MODE: ${{ inputs.mode }} - ADDITIONAL_LOCK: ${{ inputs.additional-lock }} - ADDITIONAL_GROUPS: ${{ inputs.additional-lock-groups }} - ADDITIONAL_SELECTION: ${{ inputs.additional-lock-selection }} - MATRIX_PYTHON: ${{ inputs.additional-matrix-python }} - MATRIX_PLATFORMS: ${{ inputs.additional-matrix-platforms }} - MATRIX_IMPLEMENTATIONS: ${{ inputs.additional-matrix-implementations }} run: | set -euo pipefail groups=() @@ -86,14 +105,42 @@ runs: python3 "$GITHUB_ACTION_PATH/../tools/check_nab_lock.py" \ "$RUNNER_TEMP/pylock-before-check.toml" pylock.toml fi - if [[ -z "$ADDITIONAL_LOCK" ]]; then - if [[ "$ADDITIONAL_SELECTION" != build-or-groups || - -n "$ADDITIONAL_GROUPS$MATRIX_PYTHON$MATRIX_PLATFORMS$MATRIX_IMPLEMENTATIONS" ]]; then - echo '::error::secondary matrix inputs require additional-lock' >&2 - exit 1 - fi - exit 0 - fi + - name: Resolve all project groups and extras + if: steps.producer.outputs.name == 'release-creator' + shell: bash + env: + MODE: ${{ inputs.mode }} + run: | + set -euo pipefail + case "$MODE" in + check) uv run --no-project --python "$RUNNER_TEMP/project-lock-tools" release-creator locks --check ;; + refresh) uv run --no-project --python "$RUNNER_TEMP/project-lock-tools" release-creator locks ;; + *) + echo "::error::invalid lock mode: $MODE" >&2 + exit 1 + ;; + esac + - name: Resolve optional Nab matrix + if: inputs.additional-lock != '' + shell: bash + env: + MODE: ${{ inputs.mode }} + ADDITIONAL_LOCK: ${{ inputs.additional-lock }} + ADDITIONAL_GROUPS: ${{ inputs.additional-lock-groups }} + ADDITIONAL_SELECTION: ${{ inputs.additional-lock-selection }} + MATRIX_PYTHON: ${{ inputs.additional-matrix-python }} + MATRIX_PLATFORMS: ${{ inputs.additional-matrix-platforms }} + MATRIX_IMPLEMENTATIONS: ${{ inputs.additional-matrix-implementations }} + run: | + set -euo pipefail + case "$MODE" in + check) refresh=() ;; + refresh) refresh=(--upgrade) ;; + *) + echo "::error::invalid lock mode: $MODE" >&2 + exit 1 + ;; + esac if [[ "$ADDITIONAL_LOCK" != pylock.*.toml || "$ADDITIONAL_LOCK" == */* || ! -f "$ADDITIONAL_LOCK" || -z "$MATRIX_PYTHON" || -z "$MATRIX_PLATFORMS" || -z "$MATRIX_IMPLEMENTATIONS" ]]; then diff --git a/tests/check_hook_update_cooldown/test_cooldown.py b/tests/check_hook_update_cooldown/test_cooldown.py index 0e9ec5d..fb707b9 100644 --- a/tests/check_hook_update_cooldown/test_cooldown.py +++ b/tests/check_hook_update_cooldown/test_cooldown.py @@ -6,6 +6,7 @@ import subprocess from datetime import UTC, datetime, timedelta from pathlib import Path +from typing import Literal import pytest import yaml @@ -27,10 +28,26 @@ def test_action_uses_scoped_job_token() -> None: assert caller["with"]["release-app-client-id"] == "${{ vars.RELEASE_APP_CLIENT_ID }}" +def test_action_checks_app_hook_closure_from_pull_request_head() -> None: + steps = yaml.safe_load((Path(__file__).resolve().parents[2] / "check-hook-update-cooldown/action.yml").read_text())[ + "runs" + ]["steps"] + source = next(step for step in steps if step.get("name") == "Read the base and proposed hook configurations") + closure = next(step for step in steps if step.get("name") == "Verify managed hook dependency closure") + + assert '"repos/$head_repository/contents/.pre-commit-config.yaml?ref=$HEAD_SHA"' in source["run"] + assert closure["if"] == "steps.identity.outputs.changed == 'true'" + assert 'cp "$RUNNER_TEMP/proposed-hooks.yaml" "$RUNNER_TEMP/hook-closure/.pre-commit-config.yaml"' in closure["run"] + assert "release-creator hooks" in closure["run"] + assert '--check --minimum-days "$MINIMUM_DAYS"' in closure["run"] + assert closure["env"]["MINIMUM_DAYS"] == "${{ inputs.minimum-days }}" + + @pytest.mark.parametrize( ("author", "revision", "frozen_base", "accepted"), [ - pytest.param("gaborbernat", "v2", False, True, id="human"), + pytest.param("gaborbernat", "v1", False, True, id="human-cooled-revision"), + pytest.param("gaborbernat", "v2", False, False, id="human-young-revision"), pytest.param("pre-commit-ci[bot]", "v1", False, True, id="cooled-revision-on-new-head"), pytest.param("pre-commit-ci[bot]", "v0.5", False, True, id="older-cooled-revision"), pytest.param("pre-commit-ci[bot]", "1.0", False, True, id="cooled-alias"), @@ -53,43 +70,130 @@ def test_cooldown_checks_hook_revision( server, environment = github_api repository = hook_repository(tmp_path) expect_pull(server, author=author) - if author.endswith("[bot]"): - base_revision = ( - subprocess.run( - ["git", "-C", str(repository), "rev-parse", "v0^{}"], - text=True, - capture_output=True, - check=True, - ).stdout.strip() - if frozen_base - else "v0" - ) - if revision == "sha-v0.5": - revision = subprocess.run( - ["git", "-C", str(repository), "rev-parse", "v0.5^{}"], - text=True, - capture_output=True, - check=True, - ).stdout.strip() - expect_config(server, "b" * 40, repository, base_revision) - expect_config(server, "a" * 40, repository, revision) + base_revision = ( + subprocess.run( + ["git", "-C", str(repository), "rev-parse", "v0^{}"], + text=True, + capture_output=True, + check=True, + ).stdout.strip() + if frozen_base + else "v0" + ) + if revision == "sha-v0.5": + revision = subprocess.run( + ["git", "-C", str(repository), "rev-parse", "v0.5^{}"], + text=True, + capture_output=True, + check=True, + ).stdout.strip() + expect_config(server, "b" * 40, repository, base_revision) + expect_config(server, "a" * 40, repository, revision) result = run_cooldown(action_scripts, tmp_path, environment) assert (result.returncode == 0) is accepted, result.stderr assert [request.path for request, _ in server.log] == [ "/api/v3/repos/tox-dev/fixture/pulls/7", - *( - [ - "/api/v3/repos/tox-dev/fixture/pulls/7/files", - "/api/v3/repos/tox-dev/fixture/actions/runs", - "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", - "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", - ] - if author.endswith("[bot]") - else [] + "/api/v3/repos/tox-dev/fixture/pulls/7/files", + "/api/v3/repos/tox-dev/fixture/actions/runs", + "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", + "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", + ] + + +@pytest.mark.parametrize( + ("observed_days", "accepted"), + [pytest.param(10, True, id="cooled"), pytest.param(0, False, id="new")], +) +def test_cooldown_checks_new_human_hook_repository( + tmp_path: Path, + action_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + observed_days: int, + accepted: bool, +) -> None: + server, environment = github_api + repository = hook_repository(tmp_path) + expect_pull(server, author="gaborbernat", observed_days=observed_days) + server.expect_request( + "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", + method="GET", + query_string=f"ref={'b' * 40}", + ).respond_with_json({"content": base64.b64encode(b"repos: []\n").decode()}) + expect_config(server, "a" * 40, repository, "v1", hook=" - id: sample\n args: [--strict]\n") + if not accepted: + expect_historical_runs(server, older_head=None) + + result = run_cooldown(action_scripts, tmp_path, environment) + + assert (result.returncode == 0) is accepted, result.stderr + if not accepted: + assert "hook revision has not been observed" in result.stderr + + +def test_cooldown_checks_fork_head_configuration( + tmp_path: Path, action_scripts: Path, github_api: tuple[HTTPServer, dict[str, str]] +) -> None: + server, environment = github_api + expect_pull(server, author="gaborbernat", head_repository="gaborbernat/fixture") + for repository, sha, content in ( + ("tox-dev/fixture", "b" * 40, "repos: []\n"), + ( + "gaborbernat/fixture", + "a" * 40, + ( + "repos:\n - repo: local\n hooks:\n - id: check\n name: check\n" + " entry: true\n language: system\n" + ), ), + ): + server.expect_request( + f"/api/v3/repos/{repository}/contents/.pre-commit-config.yaml", method="GET", query_string=f"ref={sha}" + ).respond_with_json({"content": base64.b64encode(content.encode()).decode()}) + + result = run_cooldown(action_scripts, tmp_path, environment) + + assert result.returncode == 0, result.stderr + assert [request.path for request, _ in server.log if request.path.endswith("/.pre-commit-config.yaml")] == [ + "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", + "/api/v3/repos/gaborbernat/fixture/contents/.pre-commit-config.yaml", ] +@pytest.mark.parametrize( + ("minimum_days", "accepted"), + [pytest.param("7", True, id="default"), pytest.param("1000", False, id="configured-cooldown")], +) +def test_cooldown_checks_human_local_hook_dependencies( + tmp_path: Path, + action_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + minimum_days: str, + accepted: bool, +) -> None: + server, environment = github_api + expect_pull(server, author="gaborbernat", observed_days=1100) + server.expect_request( + "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", + method="GET", + query_string=f"ref={'b' * 40}", + ).respond_with_json({"content": base64.b64encode(b"repos: []\n").decode()}) + proposed = ( + "repos:\n - repo: local\n hooks:\n - id: check\n name: check\n" + " entry: python\n language: python\n" + " additional_dependencies:\n - packaging==25.0\n" + ) + server.expect_request( + "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", + method="GET", + query_string=f"ref={'a' * 40}", + ).respond_with_json({"content": base64.b64encode(proposed.encode()).decode()}) + + result = run_cooldown(action_scripts, tmp_path, environment, minimum_days=minimum_days) + + assert (result.returncode == 0) is accepted, result.stderr + assert (tmp_path / "hook-closure/.pre-commit-config.yaml").read_text() == proposed + + def test_cooldown_skips_bot_without_hook_changes( tmp_path: Path, action_scripts: Path, github_api: tuple[HTTPServer, dict[str, str]] ) -> None: @@ -103,16 +207,47 @@ def test_cooldown_skips_bot_without_hook_changes( ] -def test_cooldown_allows_human_fork( +def test_cooldown_skips_unrelated_human_fork( tmp_path: Path, action_scripts: Path, github_api: tuple[HTTPServer, dict[str, str]] ) -> None: server, environment = github_api - expect_pull(server, author="gaborbernat", head_repository="gaborbernat/fixture") + expect_pull(server, author="gaborbernat", head_repository="gaborbernat/fixture", hook_update=False) result = run_cooldown(action_scripts, tmp_path, environment) assert result.returncode == 0, result.stderr - assert [request.path for request, _ in server.log] == ["/api/v3/repos/tox-dev/fixture/pulls/7"] + assert [request.path for request, _ in server.log] == [ + "/api/v3/repos/tox-dev/fixture/pulls/7", + "/api/v3/repos/tox-dev/fixture/pulls/7/files", + ] + + +@pytest.mark.parametrize( + "dependency_location", + [pytest.param("project", id="project"), pytest.param("manifest", id="manifest")], +) +def test_cooldown_rejects_rev_only_hook_with_new_dependency( + tmp_path: Path, + action_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + dependency_location: Literal["project", "manifest"], +) -> None: + server, environment = github_api + repository = hook_repository(tmp_path, dependency_location=dependency_location) + expect_pull(server, author="pre-commit-ci[bot]") + for ref, tag in (("b" * 40, "v0"), ("a" * 40, "v1")): + revision = subprocess.run( + ["git", "-C", str(repository), "rev-parse", f"{tag}^{{}}"], + text=True, + capture_output=True, + check=True, + ).stdout.strip() + expect_config(server, ref, repository, revision) + + result = run_cooldown(action_scripts, tmp_path, environment) + + assert result.returncode != 0 + assert "hook is missing managed dependencies" in result.stderr @pytest.mark.parametrize( @@ -131,8 +266,14 @@ def test_cooldown_checks_generated_update_branch( expect_pull( server, author="release-app[bot]", branch="automation/update-release-tools", observed_days=observed_days ) - expect_config(server, "b" * 40, repository, "v0") - expect_config(server, "a" * 40, repository, "v1") + for ref, tag in (("b" * 40, "v0"), ("a" * 40, "v1")): + revision = subprocess.run( + ["git", "-C", str(repository), "rev-parse", f"{tag}^{{}}"], + text=True, + capture_output=True, + check=True, + ).stdout.strip() + expect_config(server, ref, repository, revision) if not accepted: expect_historical_runs(server, older_head=None) @@ -142,6 +283,39 @@ def test_cooldown_checks_generated_update_branch( assert len(server.log) >= 4 +@pytest.mark.parametrize( + "marker", + [ + pytest.param('python_version >= "3.15"', id="active"), + pytest.param('python_version < "3.15"', id="inactive"), + ], +) +def test_cooldown_accepts_managed_pin_with_python_marker( + tmp_path: Path, + action_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + marker: str, +) -> None: + server, environment = github_api + source = ( + "repos:\n - repo: local\n hooks:\n - id: lint\n name: lint\n" + " entry: echo\n language: python\n" + " additional_dependencies:\n" + f" - 'packaging>=24,<25; {marker}'\n" + f" - 'packaging==24.2; {marker}' # release-creator managed\n" + ) + expect_pull(server, author="release-app[bot]", branch="automation/update-release-tools") + for ref in ("b" * 40, "a" * 40): + server.expect_request( + "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", method="GET", query_string=f"ref={ref}" + ).respond_with_json({"content": base64.b64encode(source.encode()).decode()}) + + result = run_cooldown(action_scripts, tmp_path, environment | {"RELEASE_APP_CLIENT_ID": "Iv1.release"}) + + assert result.returncode == 0, result.stderr + assert (tmp_path / "hook-closure/.pre-commit-config.yaml").read_text() == source + + @pytest.mark.parametrize( ("client_id", "head_repository"), [ @@ -171,7 +345,8 @@ def test_cooldown_rejects_untrusted_updater_identity( assert ( "different App identity" in result.stderr if head_repository == "tox-dev/fixture" - else [request.path for request, _ in server.log] == ["/api/v3/repos/tox-dev/fixture/pulls/7"] + else [request.path for request, _ in server.log] + == ["/api/v3/repos/tox-dev/fixture/pulls/7", "/api/v3/repos/tox-dev/fixture/pulls/7/files"] ) @@ -322,10 +497,16 @@ def test_cooldown_matches_revision_after_repository_list_change( expect_config(server, "b" * 40, repository, "v0") expect_config(server, "a" * 40, repository, "v1") expect_historical_runs(server, older_head="c" * 40) + prior_revision = subprocess.run( + ["git", "-C", str(repository), "rev-parse", "v1^{}"], + text=True, + capture_output=True, + check=True, + ).stdout.strip() historical = ( "repos:\n" " - repo: https://example.invalid/another-hook\n rev: v2\n hooks: []\n" - f" - repo: {repository.as_uri()}\n rev: v1\n hooks:\n - id: sample\n" + f" - repo: {repository.as_uri()}\n rev: {prior_revision}\n hooks:\n - id: sample\n" ) server.expect_request( "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", @@ -428,12 +609,11 @@ def expect_pull( "updated_at": (datetime.now(UTC) - timedelta(days=updated_days)).strftime("%Y-%m-%dT%H:%M:%SZ"), } ) - if author.endswith("[bot]"): - server.expect_request("/api/v3/repos/tox-dev/fixture/pulls/7/files", method="GET").respond_with_json( - [{"filename": ".pre-commit-config.yaml" if hook_update else "pyproject.toml"}] - ) - if author.endswith("[bot]") and hook_update: - if branch == "automation/update-release-tools": + server.expect_request("/api/v3/repos/tox-dev/fixture/pulls/7/files", method="GET").respond_with_json( + [{"filename": ".pre-commit-config.yaml" if hook_update else "pyproject.toml"}] + ) + if hook_update: + if author.endswith("[bot]") and branch == "automation/update-release-tools": server.expect_request("/api/v3/apps/release-app", method="GET").respond_with_json( {"slug": "release-app", "client_id": app_client_id} ) @@ -448,7 +628,7 @@ def expect_pull( else [ { "event": "pull_request", - "head_repository": {"full_name": "tox-dev/fixture"}, + "head_repository": {"full_name": head_repository}, "head_branch": branch, "head_sha": head, "pull_requests": [{"number": 7}], @@ -470,6 +650,17 @@ def expect_config( hook: str = " - id: sample\n", ) -> None: revisions = (revision,) if isinstance(revision, str) else revision + frozen = tuple( + item + if len(item) == 40 + else subprocess.run( + ["git", "-C", str(repository), "rev-parse", f"{item}^{{}}"], + text=True, + capture_output=True, + check=True, + ).stdout.strip() + for item in revisions + ) server.expect_request( "/api/v3/repos/tox-dev/fixture/contents/.pre-commit-config.yaml", method="GET", query_string=f"ref={sha}" ).respond_with_json( @@ -478,7 +669,7 @@ def expect_config( ( "repos:\n" + "".join( - f" - repo: {repository.as_uri()}\n rev: {item}\n hooks:\n{hook}" for item in revisions + f" - repo: {repository.as_uri()}\n rev: {item}\n hooks:\n{hook}" for item in frozen ) ).encode() ).decode() @@ -510,21 +701,35 @@ def expect_historical_runs( ) -def hook_repository(tmp_path: Path) -> Path: +def hook_repository(tmp_path: Path, *, dependency_location: Literal["project", "manifest"] | None = None) -> Path: repository = tmp_path / "hook-repo" repository.mkdir() subprocess.run(["git", "init", "-q", str(repository)], check=True) subprocess.run(["git", "-C", str(repository), "config", "user.name", "Hook test"], check=True) subprocess.run(["git", "-C", str(repository), "config", "user.email", "hooks@example.org"], check=True) (repository / ".pre-commit-hooks.yaml").write_text( - "- id: sample\n name: sample\n entry: true\n language: system\n" + f"- id: sample\n name: sample\n entry: true\n language: {'python' if dependency_location else 'system'}\n" ) + if dependency_location: + (repository / "pyproject.toml").write_text( + '[build-system]\nrequires = ["setuptools==80.9.0"]\nbuild-backend = "setuptools.build_meta"\n' + '[project]\nname = "hook-fixture"\nversion = "0.1.0"\ndependencies = []\n' + ) + package = repository / "src/hook_fixture" + package.mkdir(parents=True) + (package / "__init__.py").write_text('"""Hook fixture."""\n') subprocess.run(["git", "-C", str(repository), "add", "."], check=True) subprocess.run(["git", "-C", str(repository), "commit", "-qm", "Add hook"], check=True) for name, days in (("v0", 20), ("v0.5", 15), ("v1", 10), ("v2", 1)): date = (datetime.now(UTC) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ") (repository / "version").write_text(name) - subprocess.run(["git", "-C", str(repository), "add", "version"], check=True) + if dependency_location == "project" and name == "v1": + project = repository / "pyproject.toml" + project.write_text(project.read_text().replace("dependencies = []", 'dependencies = ["packaging==24.2"]')) + if dependency_location == "manifest" and name == "v1": + manifest = repository / ".pre-commit-hooks.yaml" + manifest.write_text(f"{manifest.read_text()} additional_dependencies: [packaging==24.2]\n") + subprocess.run(["git", "-C", str(repository), "add", "."], check=True) subprocess.run( ["git", "-C", str(repository), "commit", "-qm", name], env=os.environ | {"GIT_AUTHOR_DATE": date, "GIT_COMMITTER_DATE": date}, diff --git a/tests/check_hook_update_cooldown/test_frozen_hooks.py b/tests/check_hook_update_cooldown/test_frozen_hooks.py new file mode 100644 index 0000000..dbb5321 --- /dev/null +++ b/tests/check_hook_update_cooldown/test_frozen_hooks.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import runpy +import subprocess +import sys +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize( + ("revision", "accepted"), + [ + pytest.param("a" * 40, True, id="frozen"), + pytest.param("v1.0.0", False, id="tag"), + pytest.param("a" * 12, False, id="short-sha"), + pytest.param("A" * 40, False, id="uppercase-sha"), + ], +) +def test_frozen_hook_revision(tmp_path: Path, revision: str, accepted: bool) -> None: + config = tmp_path / ".pre-commit-config.yaml" + config.write_text(f"repos:\n - repo: https://example.org/hooks\n rev: {revision}\n hooks: []\n") + result = subprocess.run( + [sys.executable, str(Path(__file__).resolve().parents[2] / "tools/check_frozen_hooks.py"), str(config)], + text=True, + capture_output=True, + check=False, + ) + + assert (result.returncode == 0) is accepted + if not accepted: + assert "full lowercase commit SHA" in result.stderr + + +def test_frozen_hook_revision_allows_local_hook(tmp_path: Path) -> None: + config = tmp_path / ".pre-commit-config.yaml" + config.write_text("repos:\n - repo: local\n hooks: []\n") + result = subprocess.run( + [sys.executable, str(Path(__file__).resolve().parents[2] / "tools/check_frozen_hooks.py"), str(config)], + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + + +@pytest.mark.parametrize( + ("config_text", "message"), + [ + pytest.param("repos: null\n", "repository list", id="missing-list"), + pytest.param("repos:\n - local\n", "mapping with a URL", id="invalid-repository"), + ], +) +def test_frozen_hook_revision_rejects_malformed_config(tmp_path: Path, config_text: str, message: str) -> None: + config = tmp_path / ".pre-commit-config.yaml" + config.write_text(config_text) + result = subprocess.run( + [sys.executable, str(Path(__file__).resolve().parents[2] / "tools/check_frozen_hooks.py"), str(config)], + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode != 0 + assert message in result.stderr + + +def test_frozen_hook_import_has_no_cli_side_effect(capsys: pytest.CaptureFixture[str]) -> None: + runpy.run_path(str(Path(__file__).resolve().parents[2] / "tools/check_frozen_hooks.py"), run_name="frozen_hooks") + + assert capsys.readouterr() == ("", "") diff --git a/tests/check_hook_update_cooldown/test_hook_config.py b/tests/check_hook_update_cooldown/test_hook_config.py index 01d5501..b43da45 100644 --- a/tests/check_hook_update_cooldown/test_hook_config.py +++ b/tests/check_hook_update_cooldown/test_hook_config.py @@ -33,12 +33,12 @@ def _response(name: str, pypi_file: dict[str, object]) -> BytesIO: @pytest.fixture -def check_config(mocker: MockerFixture, pypi_request: MagicMock) -> Callable[[Path, Path, bool, int], None]: +def check_config(mocker: MockerFixture, pypi_request: MagicMock) -> Callable[..., None]: namespace = runpy.run_path( str(Path(__file__).resolve().parents[2] / "tools/check_hook_config.py"), run_name="check_hook_config" ) mocker.patch.dict(namespace["check_config"].__globals__, {"urlopen": pypi_request}) - return cast("Callable[[Path, Path, bool, int], None]", namespace["check_config"]) + return cast("Callable[..., None]", namespace["check_config"]) @pytest.fixture @@ -104,6 +104,27 @@ def test_app_update_accepts_cooled_managed_pin( assert pypi_request.call_args.args[0].full_url == "https://pypi.org/pypi/other/1.2.0/json" +def test_app_update_allows_removed_managed_pin_with_cooled_direct_change( + configs: tuple[Path, Path], check_config: Callable[..., None], pypi_request: MagicMock +) -> None: + base, proposed = configs + base.write_text( + base.read_text().replace( + " - example==1.0.0\n", + ' - example==1.0.0\n - "other==1.0.0" # release-creator managed\n', + ) + ) + proposed.write_text( + base.read_text() + .replace("example==1.0.0", "example==1.2.0") + .replace(' - "other==1.0.0" # release-creator managed\n', "") + ) + + check_config(base, proposed, True, 7) + + assert pypi_request.call_args.args[0].full_url == "https://pypi.org/pypi/example/1.2.0/json" + + @pytest.mark.parametrize( ("replacement", "message"), [ @@ -130,6 +151,57 @@ def test_other_bot_cannot_change_pin(configs: tuple[Path, Path], check_config: C check_config(base, proposed, False, 7) +@pytest.mark.parametrize("human", [pytest.param(True, id="human"), pytest.param(False, id="bot")]) +def test_hook_config_allows_human_to_add_repository_and_options( + configs: tuple[Path, Path], check_config: Callable[..., None], human: bool +) -> None: + base, proposed = configs + proposed.write_text( + base.read_text() + + " - repo: https://example.org/another-hook\n rev: 0123456789012345678901234567890123456789\n" + + " hooks:\n - id: another\n args: [--strict]\n" + ) + + if human: + check_config(base, proposed, False, 7, human=True) + else: + with pytest.raises(ValueError, match="beyond revisions"): + check_config(base, proposed, False, 7) + + +def test_hook_config_rejects_duplicate_keys_in_human_change( + configs: tuple[Path, Path], check_config: Callable[..., None] +) -> None: + base, proposed = configs + proposed.write_text(base.read_text().replace(" rev: old\n", " rev: old\n rev: new\n")) + + with pytest.raises(ValueError, match="duplicate mapping keys"): + check_config(base, proposed, False, 7, human=True) + + +def test_hook_config_rejects_human_change_without_repositories( + configs: tuple[Path, Path], check_config: Callable[..., None] +) -> None: + base, proposed = configs + proposed.write_text("repos: null\n") + + with pytest.raises(TypeError, match="hook repositories must be lists"): + check_config(base, proposed, False, 7, human=True) + + +def test_human_cannot_change_managed_pin(configs: tuple[Path, Path], check_config: Callable[..., None]) -> None: + base, proposed = configs + base.write_text( + base.read_text().replace( + " - example==1.0.0\n", ' - "example==1.0.0" # release-creator managed\n' + ) + ) + proposed.write_text(base.read_text().replace("example==1.0.0", "example==1.2.0")) + + with pytest.raises(ValueError, match="managed hook dependencies require the release App"): + check_config(base, proposed, False, 7, human=True) + + def test_app_update_keeps_direct_range( configs: tuple[Path, Path], check_config: Callable[..., None], pypi_file: dict[str, object], pypi_request: MagicMock ) -> None: @@ -146,18 +218,28 @@ def test_app_update_keeps_direct_range( assert pypi_request.call_args.args[0].full_url == "https://pypi.org/pypi/other/1.2.0/json" -def test_app_update_rejects_managed_pin_removal(configs: tuple[Path, Path], check_config: Callable[..., None]) -> None: +@pytest.mark.parametrize( + "marker", + [ + pytest.param('python_version >= "3.15"', id="active"), + pytest.param('python_version < "3.15"', id="inactive"), + ], +) +def test_app_update_accepts_managed_pin_for_ranged_direct_root( + configs: tuple[Path, Path], check_config: Callable[..., None], pypi_request: MagicMock, marker: str +) -> None: base, proposed = configs - base.write_text( + base.write_text(base.read_text().replace("example==1.0.0", f"example>=1; {marker}")) + proposed.write_text( base.read_text().replace( - " - example==1.0.0\n", - ' - example==1.0.0\n - "other==1.0.0" # release-creator managed\n', + f" - example>=1; {marker}\n", + f" - example>=1; {marker}\n - 'example==1.2.0; {marker}' # release-creator managed\n", ) ) - proposed.write_text(configs[0].read_text().replace(' - "other==1.0.0" # release-creator managed\n', "")) - with pytest.raises(ValueError, match="removes managed dependencies"): - check_config(base, proposed, True, 7) + check_config(base, proposed, True, 7) + + assert pypi_request.call_args.args[0].full_url == "https://pypi.org/pypi/example/1.2.0/json" def test_app_update_accepts_managed_pin_refresh( diff --git a/tests/check_hook_update_cooldown/test_workflow.py b/tests/check_hook_update_cooldown/test_workflow.py index 8532de0..0427385 100644 --- a/tests/check_hook_update_cooldown/test_workflow.py +++ b/tests/check_hook_update_cooldown/test_workflow.py @@ -29,7 +29,7 @@ def test_required_gate_checks_cooldown_result(event_name: str, cooldown_result: "result": cooldown_result if name == "hook-cooldown" else "skipped" - if event_name == "push" and name in {"changelog", "hook-cooldown", "release-tools-preview"} + if event_name == "push" and name == "hook-cooldown" else "success" } for name in workflow["jobs"]["gate"]["needs"] @@ -42,3 +42,71 @@ def test_required_gate_checks_cooldown_result(event_name: str, cooldown_result: check=False, ) assert (result.returncode == 0) is accepted, result.stderr + + +def test_required_gate_does_not_repeat_changelog_lock_check() -> None: + root = Path(__file__).resolve().parents[2] + check = yaml.load((root / ".github/workflows/check.yaml").read_text(), Loader=yaml.BaseLoader) + news = yaml.load((root / ".github/workflows/news.yaml").read_text(), Loader=yaml.BaseLoader) + + assert "release-tools-preview" not in check["jobs"] + assert check["jobs"]["gate"]["needs"] == ["check", "shell", "test", "type", "hook-cooldown"] + assert news["jobs"]["changelog"]["steps"][-1]["uses"] == "$/validate-news" + + +@pytest.mark.parametrize( + ("workflow_name", "job_name"), + [ + pytest.param("prepare-towncrier-release", "assemble", id="prepare"), + pytest.param("validate-python-release", "source-check", id="python-release"), + pytest.param("release", "check", id="self-release"), + ], +) +def test_tagged_source_checks_frozen_hooks_before_prek(workflow_name: str, job_name: str) -> None: + root = Path(__file__).resolve().parents[2] + workflow = yaml.load((root / ".github/workflows" / f"{workflow_name}.yaml").read_text(), Loader=yaml.BaseLoader) + steps = workflow["jobs"][job_name]["steps"] + + assert next(index for index, step in enumerate(steps) if step.get("uses") == "$/verify-frozen-hooks") < next( + index for index, step in enumerate(steps) if "prek" in step.get("run", "") + ) + + +@pytest.mark.parametrize( + ("path", "step_name"), + [ + pytest.param(".github/workflows/check.yaml", "Check repository formatting", id="pr-format"), + pytest.param(".github/workflows/check.yaml", "Check inline shell", id="pr-inline-shell"), + pytest.param(".github/workflows/prepare-towncrier-release.yaml", "Install prek hooks", id="prepare-install"), + pytest.param(".github/workflows/release.yaml", "Check tagged source", id="self-release"), + pytest.param(".github/workflows/validate-python-release.yaml", "Run tagged source checks", id="python-release"), + pytest.param("prepare-towncrier/action.yml", "Format changelog", id="prepare-format"), + pytest.param("validate-release-patch/action.yml", "Run prek on release patch", id="patch-validation"), + ], +) +def test_hook_execution_uses_install_time_cutoff(path: str, step_name: str) -> None: + document = yaml.load((Path(__file__).resolve().parents[2] / path).read_text(), Loader=yaml.BaseLoader) + steps = ( + document["runs"]["steps"] + if path.endswith("action.yml") + else [step for job in document["jobs"].values() for step in job["steps"]] + ) + + step = next(step for step in steps if step.get("name") == step_name) + assert step["env"]["UV_EXCLUDE_NEWER"] == "7 days" + assert step["env"]["UV_NO_CONFIG"] == "1" + assert step["env"]["UV_NO_SOURCES"] == "1" + unset_names = {name for line in step["run"].splitlines() if line.startswith("unset ") for name in line.split()[1:]} + assert { + "UV_CONFIG_FILE", + "UV_INDEX", + "UV_INDEX_URL", + "UV_DEFAULT_INDEX", + "UV_EXTRA_INDEX_URL", + "UV_FIND_LINKS", + "UV_EXCLUDE_NEWER_PACKAGE", + "UV_INSECURE_HOST", + "UV_SYSTEM_CERTS", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + } <= unset_names diff --git a/tests/conftest.py b/tests/conftest.py index d5f221e..c4555e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -332,8 +332,9 @@ def build_repository(source_project: Path, nab_executable: Path, monkeypatch: py ) listings = cast("dict[str, list[_PyPIFile]]", json.loads(Path(os.environ["GW_PYPI_METADATA_FILE"]).read_text())) for name, files in _lock_metadata(source_project / "pylock.toml").items(): - existing = {file["filename"] for file in listings.get(name, [])} - listings.setdefault(name, []).extend(file for file in files if file["filename"] not in existing) + merged = {file["filename"]: file for file in listings.get(name, [])} + merged.update({file["filename"]: file for file in files}) + listings[name] = list(merged.values()) metadata = source_project.parent / "pypi-metadata.json" metadata.write_text(json.dumps(listings)) monkeypatch.setenv("GW_PYPI_METADATA_FILE", str(metadata)) diff --git a/tests/export_build_constraints/test_export.py b/tests/export_build_constraints/test_export.py index 83cce9b..ceaa111 100644 --- a/tests/export_build_constraints/test_export.py +++ b/tests/export_build_constraints/test_export.py @@ -30,6 +30,41 @@ def test_export_build_constraints_uses_checked_in_locks(tmp_path: Path, action_s assert output == next(tmp_path.glob("build-constraints-*.txt")) +def test_export_build_constraints_accepts_tool_only_project_without_build_group( + tmp_path: Path, action_scripts: Path +) -> None: + source = Path(__file__).resolve().parents[2] + (tmp_path / "pyproject.toml").write_bytes((source / "pyproject.toml").read_bytes()) + document = tomlkit.parse((source / "pylock.toml").read_text()) + document["dependency-groups"].remove("build") + (tmp_path / "pylock.toml").write_text(tomlkit.dumps(document)) + outputs: dict[str, str] = {} + + result = action(tmp_path, tmp_path, action_scripts, "export-build-constraints", outputs, version="") + + assert result.returncode == 0, result.stderr + assert "setuptools==" in Path(outputs["path"]).read_text() + + +def test_export_build_constraints_writes_empty_file_without_build_selection(tmp_path: Path) -> None: + source = Path(__file__).resolve().parents[2] + document = tomlkit.parse((source / "pylock.toml").read_text()) + document["dependency-groups"].remove("build") + lock = tmp_path / "pylock.toml" + lock.write_text(tomlkit.dumps(document)) + output = tmp_path / "constraints.txt" + + result = subprocess.run( + [sys.executable, str(source / "tools/build_constraints.py"), str(output), str(lock)], + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert output.read_text() == "" + + @pytest.mark.parametrize( "platform", [ @@ -75,9 +110,7 @@ def test_export_build_constraints_prefers_compatible_caller_build_pin( current = package["version"] package["version"] = version for wheel in package["wheels"]: - wheel["name"] = wheel["name"].replace(current, version) wheel["url"] = wheel["url"].replace(current, version) - package["sdist"]["name"] = package["sdist"]["name"].replace(current, version) package["sdist"]["url"] = package["sdist"]["url"].replace(current, version) caller.write_text(tomlkit.dumps(document)) output = tmp_path / "constraints.txt" @@ -183,7 +216,6 @@ def test_export_build_constraints_rejects_invalid_arguments( @pytest.mark.parametrize( ("change", "message"), [ - pytest.param("missing-build-group", "dependency groups but no build group", id="missing-build-group"), pytest.param("local-package", "must be an index version", id="local-package"), pytest.param("missing-hash", "has no SHA256 hash", id="missing-hash"), ], @@ -193,9 +225,7 @@ def test_export_build_constraints_rejects_untrusted_build_packages(tmp_path: Pat lock = tmp_path / "pylock.build.toml" write_matrix_lock(lock) content = lock.read_text() - if change == "missing-build-group": - content = content.replace("[[packages]]", 'dependency-groups = ["release"]\n[[packages]]') - elif change == "local-package": + if change == "local-package": content = content.replace('index = "https://pypi.org/simple/"', 'directory = { path = "." }') content = content.replace("[[packages.wheels]]", "") content = ( @@ -235,7 +265,6 @@ def test_export_build_constraints_rejects_conflicting_versions_without_policy(tm current = package["version"] package["version"] = "81.0.0" for artifact in [package["sdist"], *package["wheels"]]: - artifact["name"] = artifact["name"].replace(current, "81.0.0") artifact["url"] = artifact["url"].replace(current, "81.0.0") caller.write_text(tomlkit.dumps(document)) diff --git a/tests/install_locked_tools/test_install.py b/tests/install_locked_tools/test_install.py index 183eed7..0a0a74e 100644 --- a/tests/install_locked_tools/test_install.py +++ b/tests/install_locked_tools/test_install.py @@ -291,7 +291,7 @@ def test_install_locked_tools_rejects_uncooled_lock(tmp_path: Path, kind: str, m source = Path(__file__).resolve().parents[2] document = tomlkit.parse((source / "pylock.toml").read_text()) if kind == "future-creation": - document["tool"]["nab"]["created-at"] = datetime(2099, 10, 23, tzinfo=UTC) + document["tool"]["release-creator"]["created-at"] = datetime(2099, 10, 23, tzinfo=UTC) else: package = next(package for package in document["packages"] if package["name"] == "cython") package["wheels"][0]["upload-time"] = datetime(2099, 10, 23, tzinfo=UTC) @@ -368,6 +368,38 @@ def test_install_locked_tools_skips_unselected_package(tmp_path: Path) -> None: assert result.returncode == 0, result.stderr +@pytest.mark.parametrize( + ("group", "accepted"), + [pytest.param("release-check", True, id="release"), pytest.param("test", False, id="test")], +) +def test_install_locked_tools_uses_full_lock_provenance_and_selected_groups( + tmp_path: Path, group: str, accepted: bool +) -> None: + source = Path(__file__).resolve().parents[2] + document = tomlkit.parse((source / "pylock.toml").read_text()) + document["tool"]["nab"]["created-at"] = datetime(2099, 1, 1, tzinfo=UTC) + document["tool"]["release-creator"] = {"created-at": datetime(2026, 10, 24, tzinfo=UTC)} + document["dependency-groups"].append("test") + package = next(package for package in document["packages"] if package["name"] == "release-creator") + package["marker"] = '"test" in dependency_groups' + package["wheels"][0]["url"] = package["wheels"][0]["url"].replace("files.pythonhosted.org", "example.org") + for artifact in [package["sdist"], *package["wheels"]]: + artifact["upload-time"] = datetime(2026, 10, 1, tzinfo=UTC) + lock = tmp_path / "pylock.toml" + lock.write_text(tomlkit.dumps(document)) + + result = subprocess.run( + ["python", str(source / "tools/check_lock_groups.py"), str(lock), group], + text=True, + capture_output=True, + check=False, + ) + + assert (result.returncode == 0) is accepted, result.stderr + if not accepted: + assert "non-PyPI artifact" in result.stderr + + @pytest.mark.parametrize( "source_kind", ["foreign-host", "insecure-url", "local-path"], ids=["foreign-host", "insecure-url", "local-path"] ) @@ -381,8 +413,7 @@ def test_install_locked_tools_rejects_non_pypi_artifact(tmp_path: Path, source_k elif source_kind == "insecure-url": artifact["url"] = artifact["url"].replace("https://", "http://") else: - del artifact["url"] - artifact["path"] = "cython.whl" + artifact["path"] = artifact.pop("url").rsplit("/", 1)[-1] lock = tmp_path / "pylock.toml" lock.write_text(tomlkit.dumps(document)) @@ -434,7 +465,7 @@ def test_install_locked_tools_rejects_artifact_version_mismatch(tmp_path: Path) ) assert result.returncode != 0 - assert "artifact differs from package version" in result.stderr + assert "package version" in result.stderr def test_install_locked_tools_rejects_unreviewed_version_under_reviewed_floor(tmp_path: Path) -> None: @@ -444,7 +475,6 @@ def test_install_locked_tools_rejects_unreviewed_version_under_reviewed_floor(tm current_version = str(package["version"]) package["version"] = "0.6.3" for artifact in [package["sdist"], *package["wheels"]]: - artifact["name"] = artifact["name"].replace(current_version, "0.6.3") artifact["url"] = artifact["url"].replace(current_version, "0.6.3") lock = tmp_path / "pylock.toml" lock.write_text(tomlkit.dumps(document)) @@ -468,7 +498,6 @@ def test_install_locked_tools_rejects_caller_below_shared_floor(tmp_path: Path, current_version = str(package["version"]) package["version"] = version for artifact in [package["sdist"], *package["wheels"]]: - artifact["name"] = artifact["name"].replace(current_version, version) artifact["url"] = artifact["url"].replace(current_version, version) artifact["upload-time"] = datetime(2026, 10, 15, tzinfo=UTC) lock = tmp_path / "pylock.toml" @@ -496,7 +525,7 @@ def test_install_locked_tools_constraints_only_checks_lock_age(tmp_path: Path, a source = Path(__file__).resolve().parents[2] (tmp_path / "pyproject.toml").write_bytes((source / "pyproject.toml").read_bytes()) document = tomlkit.parse((source / "pylock.toml").read_text()) - document["tool"]["nab"]["created-at"] = datetime(2099, 10, 23, tzinfo=UTC) + document["tool"]["release-creator"]["created-at"] = datetime(2099, 10, 23, tzinfo=UTC) (tmp_path / "pylock.toml").write_text(tomlkit.dumps(document)) result = action(tmp_path, tmp_path, action_scripts, "install-locked-tools", version="") assert result.returncode != 0 @@ -588,7 +617,6 @@ def test_caller_version_check_uses_highest_shared_platform_floor(tmp_path: Path) package["marker"] = '"release-news" in dependency_groups and sys_platform == "win32"' package["version"] = "0.1.0" for artifact in [package["sdist"], *package["wheels"]]: - artifact["name"] = str(artifact["name"]).replace(current_version, "0.1.0") artifact["url"] = str(artifact["url"]).replace(current_version, "0.1.0") artifact["upload-time"] = datetime(2026, 10, 15, tzinfo=UTC) caller = tmp_path / "caller.toml" diff --git a/tests/install_locked_tools/test_metadata.py b/tests/install_locked_tools/test_metadata.py index 32be4e1..98ec875 100644 --- a/tests/install_locked_tools/test_metadata.py +++ b/tests/install_locked_tools/test_metadata.py @@ -91,6 +91,43 @@ def test_lock_metadata_matches_pypi( assert context.call_args.kwargs == {"cafile": certifi.where()} +@pytest.mark.parametrize( + ("producer", "uploaded", "accepted"), + [ + pytest.param("release-creator (uv)", "2021-05-05T14:18:17Z", True, id="uv-whole-second"), + pytest.param("release-creator (uv)", "2021-05-05T14:18:16Z", False, id="uv-earlier-second"), + pytest.param("test", "2021-05-05T14:18:17Z", False, id="nab-needs-fraction"), + ], +) +def test_lock_metadata_checks_producer_timestamp_precision( + lock_case: tuple[Path, Path], + pypi_listing: _PyPIListing, + mocker: MockerFixture, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + producer: str, + uploaded: str, + accepted: bool, +) -> None: + lock = lock_case[0] + text = lock.read_text().replace('created-by = "test"', f'created-by = "{producer}"') + if producer == "release-creator (uv)": + text = text.replace("[tool.nab]", "[tool.release-creator]") + lock.write_text(text.replace("2021-05-05T14:18:17.237677Z", uploaded)) + request = mocker.patch( + "urllib.request.urlopen", side_effect=lambda *_args, **_kwargs: BytesIO(json.dumps(pypi_listing).encode()) + ) + mocker.patch("time.sleep") + + if accepted: + _run_check(lock_case, monkeypatch, "--verify-pypi") + assert request.call_count == 1 + else: + with pytest.raises(SystemExit, match="2"): + _run_check(lock_case, monkeypatch, "--verify-pypi") + assert "release lock differs from PyPI metadata" in capsys.readouterr().err + + def test_lock_metadata_rejects_yanked_artifact( lock_case: tuple[Path, Path], pypi_listing: _PyPIListing, @@ -383,6 +420,22 @@ def test_lock_metadata_checks_nested_group_marker( assert "non-PyPI artifact" in capsys.readouterr().err +def test_lock_metadata_checks_package_excluding_extras( + lock_case: tuple[Path, Path], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + lock = lock_case[0] + lock.write_text( + lock.read_text() + .replace("marker = '\"release-check\" in dependency_groups'", "marker = '\"web\" not in extras'") + .replace("files.pythonhosted.org", "evil.invalid") + ) + + with pytest.raises(SystemExit, match="2"): + _run_check(lock_case, monkeypatch, "--project", str(lock_case[1])) + + assert "non-PyPI artifact" in capsys.readouterr().err + + def test_lock_metadata_checks_each_duplicate_artifact( lock_case: tuple[Path, Path], pypi_listing: _PyPIListing, diff --git a/tests/publish_jetbrains_marketplace/test_marketplace_upload.py b/tests/publish_jetbrains_marketplace/test_marketplace_upload.py index 8c625a0..52a6dd3 100644 --- a/tests/publish_jetbrains_marketplace/test_marketplace_upload.py +++ b/tests/publish_jetbrains_marketplace/test_marketplace_upload.py @@ -214,13 +214,244 @@ def test_prior_upload_on_another_commit_blocks_retry( assert result.returncode != 0 assert "earlier Marketplace upload on commit" in result.stderr assert any( - request.path == "/api/v3/repos/tox-dev/fixture/actions/workflows/release.yaml/runs" - and request.query_string == b"per_page=100" + request.path == "/api/v3/repos/tox-dev/fixture/actions/runs" + and request.query_string == b"branch=v0.1.11&per_page=100" for request, _ in github_api[0].log ) assert not httpserver.log +def test_prior_upload_from_renamed_workflow_blocks_retry( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_run=True, + previous_workflow="old-release.yaml", + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "Reconcile run 41 attempt 1 manually" in result.stderr + assert not httpserver.log + + +def test_deleted_prior_run_deployment_blocks_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_deployment=True, + previous_deployment_job_missing=True, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "Cannot inspect release deployment" in result.stderr + assert not httpserver.log + + +def test_prior_contributor_deployment_blocks_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_deployment=True, + previous_deployment_upload=False, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "prior release deployment 70 from run 41 attempt 1" in result.stderr + assert not httpserver.log + + +@pytest.mark.parametrize( + ("state", "started"), + [ + pytest.param("success", "2026-10-23T00:00:00Z", id="completed"), + pytest.param("failure", None, id="partial"), + ], +) +def test_renamed_prior_upload_deployment_blocks_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, + state: str, + started: str | None, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_deployment=True, + previous_deployment_step="Publish to Marketplace", + previous_deployment_started=started, + previous_deployment_state=state, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "prior release deployment 70 from run 41 attempt 1" in result.stderr + assert not httpserver.log + + +def test_new_tag_on_old_commit_can_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + httpserver.expect_request("/api/updates/upload", method="POST").respond_with_data("{}") + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + run_history=False, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode == 0, result.stderr + assert len(httpserver.log) == 1 + + +def test_previous_attempt_deployment_blocks_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_deployment=True, + previous_deployment_run=42, + previous_deployment_attempt=1, + current_attempt=2, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "prior release deployment" in result.stderr + assert not httpserver.log + + +def test_moved_tag_deployment_blocks_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + previous_deployment=True, + previous_deployment_sha="1" * 40, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "release deployment on another commit" in result.stderr + assert not httpserver.log + + +def test_missing_current_publisher_deployment_blocks_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + current_deployment_upload=False, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode != 0 + assert "needs a release environment deployment" in result.stderr + assert not httpserver.log + + +def test_current_run_other_deployments_allow_upload( + signed_archive: Path, + tmp_path: Path, + marketplace_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + httpserver: HTTPServer, +) -> None: + httpserver.expect_request("/api/updates/upload", method="POST").respond_with_data("{}") + result = publish_action( + tmp_path, + marketplace_scripts, + github_api, + extra_current_deployment=True, + ARCHIVE=str(signed_archive), + SHA256=sha256(signed_archive.read_bytes()).hexdigest(), + PLUGIN_ID="29973", + CHANNEL="beta", + PUBLISH_TOKEN="fixture-token", + ) + assert result.returncode == 0, result.stderr + assert len(httpserver.log) == 1 + assert any( + request.path == "/api/v3/repos/tox-dev/fixture/deployments" + and request.query_string == b"ref=v0.1.11&environment=release&per_page=100" + for request, _ in github_api[0].log + ) + + @pytest.mark.parametrize("annotated", [pytest.param(False, id="lightweight"), pytest.param(True, id="annotated")]) def test_remote_moved_tag_blocks_upload( signed_archive: Path, @@ -269,18 +500,21 @@ def test_annotated_tag_targets_release_commit( assert len(httpserver.log) == 1 +@pytest.mark.parametrize("previous_workflow", ["release.yaml", "old-release.yaml"]) def test_prior_acceptance_with_matching_artifacts_skips_upload( signed_archive: Path, tmp_path: Path, marketplace_scripts: Path, github_api: tuple[HTTPServer, dict[str, str]], httpserver: HTTPServer, + previous_workflow: str, ) -> None: result = publish_action( tmp_path, marketplace_scripts, github_api, previous_run=True, + previous_workflow=previous_workflow, accepted_archive=signed_archive, ARCHIVE=str(signed_archive), SHA256=sha256(signed_archive.read_bytes()).hexdigest(), @@ -391,6 +625,20 @@ def publish_action( previous_run: bool = False, previous_commit: str = "0" * 40, previous_step: str = "Publish signed ZIP", + previous_workflow: str = "release.yaml", + previous_deployment: bool = False, + previous_deployment_run: int = 41, + previous_deployment_attempt: int = 1, + previous_deployment_upload: bool = True, + previous_deployment_step: str = "Publish signed ZIP", + previous_deployment_started: str | None = "2026-10-23T00:00:00Z", + previous_deployment_state: str = "success", + previous_deployment_sha: str = "0" * 40, + previous_deployment_job_missing: bool = False, + extra_current_deployment: bool = False, + current_deployment_upload: bool = True, + current_attempt: int = 1, + run_history: bool = True, accepted_archive: Path | None = None, missing_receipt: bool = False, receipt_changes: dict[str, str | int] | None = None, @@ -408,15 +656,70 @@ def publish_action( server.expect_request(f"/api/v3/repos/tox-dev/fixture/git/tags/{'f' * 40}").respond_with_json( {"object": {"type": "commit", "sha": remote_commit}} ) - server.expect_request("/api/v3/repos/tox-dev/fixture/actions/workflows/release.yaml/runs").respond_with_json( - { - "workflow_runs": [ - {"id": 41, "head_branch": "v0.1.11", "head_sha": previous_commit}, - ] - if previous_run - else [] - } + runs = ( + [ + { + "id": 40, + "head_branch": "main", + "head_sha": "2" * 40, + "path": ".github/workflows/check.yaml", + } + ] + if run_history + else [] ) + if previous_run: + runs.append( + { + "id": 41, + "head_branch": "v0.1.11", + "head_sha": previous_commit, + "path": f".github/workflows/{previous_workflow}", + } + ) + server.expect_request("/api/v3/repos/tox-dev/fixture/actions/runs").respond_with_json({"workflow_runs": runs}) + deployments = [{"id": 71, "sha": "0" * 40}] + if extra_current_deployment: + deployments.append({"id": 72, "sha": "0" * 40}) + if previous_deployment: + deployments.append({"id": 70, "sha": previous_deployment_sha}) + server.expect_request("/api/v3/repos/tox-dev/fixture/deployments").respond_with_json(deployments) + for deployment in deployments: + deployment_id = int(deployment["id"]) + prior = deployment_id == 70 + run_id = previous_deployment_run if prior else 42 + attempt = previous_deployment_attempt if prior else current_attempt + job_id = deployment_id + 100 + server.expect_request(f"/api/v3/repos/tox-dev/fixture/deployments/{deployment_id}/statuses").respond_with_json( + [ + { + "log_url": f"https://github.com/tox-dev/fixture/actions/runs/{run_id}/job/{job_id}", + "state": previous_deployment_state if prior else "in_progress", + } + ] + ) + job_request = server.expect_request(f"/api/v3/repos/tox-dev/fixture/actions/jobs/{job_id}") + if prior and previous_deployment_job_missing: + job_request.respond_with_data("deleted", status=404) + else: + job_request.respond_with_json( + { + "run_id": run_id, + "run_attempt": attempt, + "steps": [ + { + "name": ( + previous_deployment_step + if prior and previous_deployment_upload + else "Publish signed ZIP" + if current_deployment_upload and not prior + else "Generate contributors" + ), + "started_at": previous_deployment_started if prior else "2026-10-23T00:00:00Z", + } + ], + } + ) if previous_run: server.expect_request("/api/v3/repos/tox-dev/fixture/actions/runs/41/jobs").respond_with_json( {"jobs": [{"run_attempt": 1, "steps": [{"name": previous_step, "started_at": "2026-10-23T00:00:00Z"}]}]} @@ -427,7 +730,10 @@ def publish_action( "id": artifact_id, "name": artifact_name, "expired": False, - "archive_download_url": f"https://localhost:{server.port}/api/v3/repos/tox-dev/fixture/actions/artifacts/{artifact_id}/zip", + "archive_download_url": ( + f"https://localhost:{server.port}/api/v3/repos/tox-dev/fixture/actions/" + f"artifacts/{artifact_id}/zip" + ), } for artifact_id, artifact_name in ( (61, "marketplace-acceptance-41-1"), @@ -487,9 +793,10 @@ def publish_action( NOTES_ARTIFACT_ID="52", NOTES_FILE=str(notes_file), GITHUB_REF="refs/tags/v0.1.11", + GITHUB_SERVER_URL="https://github.com", GITHUB_WORKFLOW_REF="tox-dev/fixture/.github/workflows/release.yaml@refs/tags/v0.1.11", GITHUB_RUN_ID="42", - GITHUB_RUN_ATTEMPT="1", + GITHUB_RUN_ATTEMPT=str(current_attempt), **environment, **settings, ) diff --git a/tests/recheck_hook_update_cooldown/test_recheck.py b/tests/recheck_hook_update_cooldown/test_recheck.py index 0c1aeff..e0a39f7 100644 --- a/tests/recheck_hook_update_cooldown/test_recheck.py +++ b/tests/recheck_hook_update_cooldown/test_recheck.py @@ -43,8 +43,13 @@ def test_recheck_schedule_has_only_required_permissions() -> None: ], ) @pytest.mark.parametrize( - "bot_login", - [pytest.param("pre-commit-ci[bot]", id="pre-commit-ci"), pytest.param("release-app[bot]", id="app")], + ("login", "author_type", "head_repository"), + [ + pytest.param("pre-commit-ci[bot]", "Bot", "tox-dev/fixture", id="pre-commit-ci"), + pytest.param("release-app[bot]", "Bot", "tox-dev/fixture", id="app"), + pytest.param("gaborbernat", "User", "tox-dev/fixture", id="human"), + pytest.param("gaborbernat", "User", "gaborbernat/fixture", id="human-fork"), + ], ) def test_recheck_reruns_failed_job( github_api: tuple[HTTPServer, dict[str, str]], @@ -57,7 +62,9 @@ def test_recheck_reruns_failed_job( configured_job: str, workflow: str, reruns: int, - bot_login: str, + login: str, + author_type: str, + head_repository: str, ) -> None: server, environment = github_api observed_at = (datetime.now(UTC) - timedelta(days=observed_age)).strftime("%Y-%m-%dT%H:%M:%SZ") @@ -65,11 +72,11 @@ def test_recheck_reruns_failed_job( [ { "number": 7, - "user": {"login": bot_login, "type": "Bot"}, + "user": {"login": login, "type": author_type}, "head": { "sha": "a" * 40, "ref": "pre-commit-ci-update-config", - "repo": {"full_name": "tox-dev/fixture"}, + "repo": {"full_name": head_repository}, }, "base": {"repo": {"full_name": "tox-dev/fixture"}}, "updated_at": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), @@ -87,7 +94,7 @@ def test_recheck_reruns_failed_job( { "id": 42, "event": "pull_request", - "head_repository": {"full_name": "tox-dev/fixture"}, + "head_repository": {"full_name": head_repository}, "head_sha": "a" * 40, "head_branch": "pre-commit-ci-update-config", "pull_requests": [{"number": 7}], diff --git a/tests/refresh_release_lock/test_cutoffs.py b/tests/refresh_release_lock/test_cutoffs.py index 0f1c045..dd0c700 100644 --- a/tests/refresh_release_lock/test_cutoffs.py +++ b/tests/refresh_release_lock/test_cutoffs.py @@ -8,6 +8,7 @@ from collections.abc import Callable from datetime import UTC, datetime, timedelta from pathlib import Path +from types import FunctionType from typing import cast import pytest @@ -16,7 +17,11 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from tools.check_nab_lock import main as check_nab_lock -from tools.nab_cutoffs import remove_expired_cutoffs +from tools.nab_cutoffs import ( + remove_expired_cutoffs, + remove_expired_release_creator_cutoffs, + validate_release_creator_cutoffs, +) @pytest.fixture @@ -77,6 +82,7 @@ def test_expire_cutoffs_removes_empty_package_table(cutoff_project: Path) -> Non pytest.param("2026-10-23T20:48:24Z", id="0.6.6"), pytest.param("2026-10-23T21:40:06Z", id="0.6.7"), pytest.param("2026-10-24T06:07:46Z", id="1.0.0"), + pytest.param("2026-10-24T09:08:40Z", id="1.1.0"), ], ) def test_expire_cutoffs_removes_each_reviewed_release_creator_floor(reviewed: str) -> None: @@ -119,6 +125,85 @@ def test_expire_cutoffs_accepts_toml_datetime(cutoff_project: Path) -> None: assert "nab" not in tomllib.loads(updated)["tool"]["nab"]["packages"] +@pytest.mark.parametrize( + ("cutoff", "accepted"), + [ + pytest.param("2026-10-24T09:08:40Z", True, id="reviewed"), + pytest.param("2026-10-24T09:08:41Z", False, id="unreviewed"), + ], +) +def test_release_creator_cutoffs_require_review(cutoff: str, accepted: bool) -> None: + project = f'[tool.release-creator.locks.exclude-newer-package]\nrelease-creator = "{cutoff}"\n' + + if accepted: + validate_release_creator_cutoffs(project, datetime(2026, 10, 17, tzinfo=UTC)) + else: + with pytest.raises(ValueError, match="unreviewed recent release-creator cutoff"): + validate_release_creator_cutoffs(project, datetime(2026, 10, 17, tzinfo=UTC)) + + +@pytest.mark.parametrize( + ("project_text", "accepted"), + [ + pytest.param("[tool.release-creator.locks]\n", True, id="release-creator-only"), + pytest.param("[tool.other]\n", False, id="no-policy"), + ], +) +def test_recent_cutoff_cli_checks_project_provider(tmp_path: Path, project_text: str, accepted: bool) -> None: + project = tmp_path / "pyproject.toml" + project.write_text(project_text) + result = subprocess.run( + [sys.executable, str(Path(__file__).resolve().parents[2] / "tools/check_recent_cutoffs.py"), str(project)], + capture_output=True, + text=True, + check=False, + ) + + assert (result.returncode == 0) is accepted + if not accepted: + assert "project has no lock cutoff policy" in result.stderr + + +@pytest.mark.parametrize( + ("locks", "error"), + [ + pytest.param("unexpected = true", ValueError, id="unknown-setting"), + pytest.param('exclude-newer-package = "recent"', TypeError, id="non-table"), + pytest.param("[tool.release-creator.locks.exclude-newer-package]\nexample = false", TypeError, id="non-time"), + ], +) +def test_release_creator_cutoffs_reject_invalid_policy(locks: str, error: type[Exception]) -> None: + with pytest.raises(error): + validate_release_creator_cutoffs(f"[tool.release-creator.locks]\n{locks}\n", datetime(2026, 10, 17, tzinfo=UTC)) + + +def test_expire_release_creator_cutoff_keeps_deliberate_hold() -> None: + project = ( + "[tool.release-creator.locks.exclude-newer-package]\n" + 'release-creator = "2026-10-24T06:07:46Z"\n' + 'bad-package = "2026-01-01T00:00:00Z"\n' + ) + + updated = remove_expired_release_creator_cutoffs(project, datetime(2026, 11, 1, tzinfo=UTC)) + + assert tomllib.loads(updated)["tool"]["release-creator"]["locks"]["exclude-newer-package"] == { + "bad-package": "2026-01-01T00:00:00Z" + } + + +def test_expire_release_creator_cutoff_removes_empty_override() -> None: + project = '[tool.release-creator.locks.exclude-newer-package]\nrelease-creator = "2026-10-24T09:08:40Z"\n' + updated = remove_expired_release_creator_cutoffs(project, datetime(2026, 11, 1, tzinfo=UTC)) + + assert "exclude-newer-package" not in updated + + +def test_expire_release_creator_cutoff_keeps_unexpired_override() -> None: + project = '[tool.release-creator.locks.exclude-newer-package]\nrelease-creator = "2026-10-24T09:08:40Z"\n' + + assert remove_expired_release_creator_cutoffs(project, datetime(2026, 10, 17, tzinfo=UTC)) == project + + def test_verify_cutoff_patch_rejects_deliberate_hold_removal(cutoff_project: Path) -> None: cutoff_project.write_text( cutoff_project.read_text().replace('[tool.nab.packages.nab]\nuploaded-prior-to = "2026-01-01T00:00:00Z"\n', "") @@ -188,6 +273,63 @@ def test_verify_cutoff_patch_rejects_early_floor_removal(cutoff_project: Path) - assert "changes beyond expired Nab cutoffs" in result.stderr +@pytest.mark.parametrize( + ("remove", "accepted"), + [pytest.param("release-creator", True, id="reviewed-expired"), pytest.param("bad-package", False, id="hold")], +) +def test_verify_cutoff_patch_limits_release_creator_removal( + cutoff_project: Path, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, remove: str, accepted: bool +) -> None: + cutoff_project.write_text( + cutoff_project.read_text() + + "[tool.release-creator.locks.exclude-newer-package]\n" + + 'release-creator = "2026-10-24T06:07:46Z"\n' + + 'bad-package = "2026-01-01T00:00:00Z"\n' + ) + subprocess.run(["git", "add", "pyproject.toml"], cwd=cutoff_project.parent, check=True, capture_output=True) + subprocess.run( + ["git", "commit", "-m", "Add release policy"], cwd=cutoff_project.parent, check=True, capture_output=True + ) + cutoff_project.write_text(cutoff_project.read_text().replace(f'{remove} = "', f'# {remove} = "')) + script = Path(__file__).resolve().parents[2] / "tools/verify_cutoff_patch.py" + monkeypatch.syspath_prepend(str(script.parent)) + main = cast("FunctionType", runpy.run_path(str(script), run_name="cutoff_patch_test")["main"]) + clock = mocker.create_autospec(datetime) + clock.now.return_value = datetime(2026, 11, 1, tzinfo=UTC) + main.__globals__["datetime"] = clock + monkeypatch.chdir(cutoff_project.parent) + monkeypatch.setattr(sys, "argv", [str(script)]) + + if accepted: + main() + else: + with pytest.raises(SystemExit, match="2"): + main() + + +def test_verify_cutoff_patch_accepts_release_creator_only_policy( + cutoff_project: Path, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture +) -> None: + cutoff_project.write_text( + '[tool.release-creator.locks.exclude-newer-package]\nrelease-creator = "2026-10-24T09:08:40Z"\n' + ) + subprocess.run(["git", "add", "pyproject.toml"], cwd=cutoff_project.parent, check=True, capture_output=True) + subprocess.run( + ["git", "commit", "-m", "Add release policy"], cwd=cutoff_project.parent, check=True, capture_output=True + ) + cutoff_project.write_text("[tool.release-creator.locks]\n") + script = Path(__file__).resolve().parents[2] / "tools/verify_cutoff_patch.py" + monkeypatch.syspath_prepend(str(script.parent)) + main = cast("FunctionType", runpy.run_path(str(script), run_name="cutoff_patch_test")["main"]) + clock = mocker.create_autospec(datetime) + clock.now.return_value = datetime(2026, 11, 1, tzinfo=UTC) + main.__globals__["datetime"] = clock + monkeypatch.chdir(cutoff_project.parent) + monkeypatch.setattr(sys, "argv", [str(script)]) + + main() + + @pytest.fixture def nab_lock_pair(tmp_path: Path) -> tuple[Path, Path]: committed = tmp_path / "committed.toml" @@ -459,7 +601,7 @@ def test_verify_cutoff_patch_reports_invalid_boolean(cutoff_project: Path) -> No check=False, ) assert result.returncode != 0 - assert "invalid Nab cutoff: True" in result.stderr + assert "invalid cutoff for Nab: True" in result.stderr def test_expire_cutoffs_preserves_disabled_package(cutoff_project: Path) -> None: @@ -478,7 +620,7 @@ def test_expire_cutoffs_preserves_disabled_package(cutoff_project: Path) -> None @pytest.mark.parametrize( ("value", "message"), [ - pytest.param("true", "invalid Nab cutoff", id="boolean"), + pytest.param("true", "invalid cutoff for example", id="boolean"), pytest.param('"2026-01-01T00:00:00"', "has no timezone", id="naive-time"), ], ) @@ -547,6 +689,23 @@ def test_expire_cutoffs_cli_writes_expired_rule( assert "package-rules" not in tomllib.loads(project.read_text())["tool"]["nab"] +def test_expire_cutoffs_cli_writes_release_creator_policy( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture +) -> None: + monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[2] / "tools")) + module = importlib.import_module("expire_cutoffs") + project = tmp_path / "pyproject.toml" + project.write_text('[tool.release-creator.locks.exclude-newer-package]\nrelease-creator = "2026-10-24T09:08:40Z"\n') + mocker.patch.object( + module, "datetime", mocker.Mock(now=mocker.Mock(return_value=datetime(2026, 11, 1, tzinfo=UTC))) + ) + monkeypatch.setattr(sys, "argv", ["expire_cutoffs", str(project)]) + + module.main() + + assert "exclude-newer-package" not in project.read_text() + + def test_expire_cutoffs_cli_rejects_missing_project(tmp_path: Path) -> None: result = subprocess.run( [ diff --git a/tests/refresh_release_lock/test_provider.py b/tests/refresh_release_lock/test_provider.py new file mode 100644 index 0000000..bec1e78 --- /dev/null +++ b/tests/refresh_release_lock/test_provider.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from pathlib import Path + +from conftest import action + + +def test_refresh_rejects_unknown_lock_producer(tmp_path: Path, action_scripts: Path) -> None: + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = "test"\nrequires-python = ">=3.15"\npackages = []\n' + ) + + result = action(tmp_path, tmp_path, action_scripts, "refresh-release-lock", version="") + + assert result.returncode != 0 + assert "lock has no recognized producer" in result.stderr + + +def test_refresh_rejects_secondary_options_without_lock(tmp_path: Path, action_scripts: Path) -> None: + result = action( + tmp_path, + tmp_path, + action_scripts, + "refresh-release-lock", + version="", + ADDITIONAL_LOCK_GROUPS="build", + ) + + assert result.returncode != 0 + assert "secondary matrix inputs require additional-lock" in result.stderr diff --git a/tests/release_workflow/test_graph.py b/tests/release_workflow/test_graph.py index 4418f06..d891593 100644 --- a/tests/release_workflow/test_graph.py +++ b/tests/release_workflow/test_graph.py @@ -179,7 +179,7 @@ def test_prepare_installs_hooks_while_building_changelog() -> None: wait = next(step for step in steps if step.get("wait") == "hooks") finalize = next(step for step in steps if step.get("name") == "Finalize release patch") assert hooks["background"] is True - assert hooks["run"] == "prek install-hooks" + assert hooks["run"].splitlines()[-1] == "prek install-hooks" assert [build["with"]["phase"], finalize["with"]["phase"]] == ["build", "finalize"] assert steps.index(hooks) < steps.index(build) < steps.index(wait) < steps.index(finalize) diff --git a/tests/upgrade_release_tools/test_upgrade.py b/tests/upgrade_release_tools/test_upgrade.py index 4724410..6b4a61c 100644 --- a/tests/upgrade_release_tools/test_upgrade.py +++ b/tests/upgrade_release_tools/test_upgrade.py @@ -64,7 +64,7 @@ def test_upgrade_locks_only_selected_groups(lock_repository: Path, tmp_path: Pat assert installed.returncode == 0, installed.stderr resolved = execute( lock_repository, - upgrade_scripts / "refresh-release-lock-3.sh", + upgrade_scripts / "refresh-release-lock-5.sh", tmp_path, LOCK_GROUPS="release-check", MODE="refresh", @@ -94,7 +94,7 @@ def test_upgrade_check_mode_preserves_lock_bytes(lock_repository: Path, tmp_path assert installed.returncode == 0, installed.stderr refreshed = execute( lock_repository, - upgrade_scripts / "refresh-release-lock-3.sh", + upgrade_scripts / "refresh-release-lock-5.sh", tmp_path, LOCK_GROUPS="release-check", MODE="refresh", @@ -138,7 +138,7 @@ def test_upgrade_preserves_deliberate_nab_hold(lock_repository: Path, tmp_path: VENV=str(tmp_path / "lock-tools"), ) assert installed.returncode == 0, installed.stderr - expired = execute(lock_repository, upgrade_scripts / "refresh-release-lock-2.sh", tmp_path) + expired = execute(lock_repository, upgrade_scripts / "refresh-release-lock-4.sh", tmp_path) assert expired.returncode == 0, expired.stderr assert tomllib.loads(project.read_text())["tool"]["nab"]["package-rules"][0]["uploaded-prior-to"] == ( "2026-01-01T00:00:00Z" @@ -158,7 +158,7 @@ def test_upgrade_emits_patch_for_allowed_change( (lock_repository / changed_path).write_text((lock_repository / changed_path).read_text() + "\n") result = execute( lock_repository, - upgrade_scripts / "refresh-release-lock-4.sh", + upgrade_scripts / "refresh-release-lock-8.sh", tmp_path, MODE="refresh", GITHUB_OUTPUT=str(tmp_path / "output"), diff --git a/tests/validate_news/test_lock_groups.py b/tests/validate_news/test_lock_groups.py index 8756174..de5308a 100644 --- a/tests/validate_news/test_lock_groups.py +++ b/tests/validate_news/test_lock_groups.py @@ -2,6 +2,7 @@ import runpy import subprocess +import sys from pathlib import Path import pytest @@ -31,7 +32,7 @@ def test_lock_groups_reads_recorded_selection(tmp_path: Path) -> None: @pytest.mark.parametrize( ("command", "message"), [ - pytest.param('["nab", "lock", "--output", "pylock.toml"]', "cannot read Nab lock groups", id="missing-flag"), + pytest.param('["nab", "lock", "--output", "pylock.toml"]', "cannot read lock groups", id="missing-flag"), pytest.param('"nab lock --groups release-news"', "must be a list of strings", id="string-command"), pytest.param('["nab", 42]', "must be a list of strings", id="non-string-argument"), pytest.param( @@ -62,6 +63,304 @@ def test_lock_groups_import_has_no_cli_side_effect(capsys: pytest.CaptureFixture assert capsys.readouterr() == ("", "") +def test_lock_groups_reads_full_project_selection(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + "[project.optional-dependencies]\nweb = []\n[dependency-groups]\nrelease-news = []\ntest = []\n" + ) + lock = tmp_path / "pylock.toml" + lock.write_text( + 'dependency-groups = ["project-runtime", "release-news", "test"]\nextras = ["web"]\ndefault-groups = []\n' + "[tool.nab]\ncreated-at = 2026-10-24T00:00:00Z\n" + "[tool.release-creator]\ncreated-at = 2026-10-24T00:00:00Z\n" + ) + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert (result.returncode, result.stdout) == (0, "release-news\n") + + +def test_lock_groups_includes_workspace_members_and_exclusions(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[tool.uv.workspace]\nmembers = ["packages/*"]\nexclude = ["packages/unused"]\n' + "[dependency-groups]\nrelease-news = []\n" + ) + for name, groups, extras in ( + ("included", "test = []\nrelease-package = []\n", "web = []\n"), + ("unused", "stale = []\n", "stale = []\n"), + ): + member = tmp_path / "packages" / name + member.mkdir(parents=True) + (member / "pyproject.toml").write_text( + f"[dependency-groups]\n{groups}[project.optional-dependencies]\n{extras}" + ) + lock = tmp_path / "pylock.toml" + lock.write_text( + 'dependency-groups = ["project-runtime", "release-news", "release-package", "test"]\n' + 'extras = ["web"]\ndefault-groups = []\n[tool.release-creator]\n' + ) + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert (result.returncode, result.stdout) == (0, "release-news release-package\n") + + +def test_lock_groups_accepts_declared_workspace_source(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "bare"\n[tool.uv.workspace]\nmembers = ["packages/*"]\n' + "[dependency-groups]\nrelease-news = []\n" + ) + for name in ("meta", "other"): + member = tmp_path / "packages" / name + member.mkdir(parents=True) + (member / "pyproject.toml").write_text( + f'[project]\nname = "{name}"\n[dependency-groups]\nrelease-{name} = []\n' + + ("[tool.uv.sources]\nbare = { workspace = true }\n" if name == "meta" else "") + ) + (tmp_path / "packages" / "notes.txt").write_text("not a project") + lock = tmp_path / "pylock.toml" + lock.write_text( + 'dependency-groups = ["project-runtime", "release-news", "release-meta", "release-other"]\n' + "extras = []\ndefault-groups = []\n[tool.release-creator]\n" + ) + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert (result.returncode, result.stdout) == (0, "release-news release-meta release-other\n") + + +@pytest.mark.parametrize( + ("groups", "extras", "message"), + [ + pytest.param('"release-news"', "[]", "invalid dependency groups", id="groups-not-list"), + pytest.param('["release-news", 1]', "[]", "invalid dependency groups", id="non-string-group"), + pytest.param('["project-runtime", "release-news"]', '"web"', "invalid extras", id="extras-not-list"), + pytest.param('["project-runtime", "release-news"]', "[1]", "invalid extras", id="non-string-extra"), + pytest.param( + '["project-runtime", "release-news"]', + '["web", "unknown"]', + "invalid or undeclared extras", + id="undeclared-extra", + ), + pytest.param( + '["project-runtime", "release-news"]', + '["web", "web"]', + "invalid or undeclared extras", + id="duplicate-extra", + ), + ], +) +def test_lock_groups_rejects_malformed_project_selections( + tmp_path: Path, groups: str, extras: str, message: str +) -> None: + (tmp_path / "pyproject.toml").write_text( + "[dependency-groups]\nrelease-news = []\n[project.optional-dependencies]\nweb = []\n" + ) + lock = tmp_path / "pylock.toml" + lock.write_text(f"dependency-groups = {groups}\nextras = {extras}\ndefault-groups = []\n[tool.release-creator]\n") + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert message in result.stderr + + +def test_lock_groups_rejects_invalid_workspace_patterns(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text("[tool.uv.workspace]\nmembers = [1]\n") + lock = tmp_path / "pylock.toml" + lock.write_text( + 'dependency-groups = ["project-runtime"]\nextras = []\ndefault-groups = []\n[tool.release-creator]\n' + ) + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert "workspace patterns must be strings" in result.stderr + + +def test_lock_groups_rejects_workspace_member_outside_project(tmp_path: Path) -> None: + project = tmp_path / "project" + project.mkdir() + (project / "pyproject.toml").write_text('[tool.uv.workspace]\nmembers = ["external"]\n') + outside = tmp_path / "external" + outside.mkdir() + (outside / "pyproject.toml").write_text("[dependency-groups]\nrelease-hidden = []\n") + (project / "external").symlink_to(outside, target_is_directory=True) + lock = project / "pylock.toml" + lock.write_text( + 'dependency-groups = ["project-runtime", "release-hidden"]\n' + "extras = []\ndefault-groups = []\n[tool.release-creator]\n" + ) + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert "workspace members must be inside the project" in result.stderr + + +@pytest.mark.parametrize( + "source", + [ + pytest.param("missing = { workspace = true }", id="undeclared-member"), + pytest.param('bare = { git = "https://example.org/bare" }', id="git-source"), + pytest.param('bare = { workspace = true, git = "https://example.org/bare" }', id="mixed-source"), + ], +) +def test_lock_groups_rejects_unapproved_workspace_sources_in_pr_and_release(tmp_path: Path, source: str) -> None: + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "bare"\nrequires-python = ">=3.15"\n' + '[tool.uv.workspace]\nmembers = ["meta"]\n[dependency-groups]\nrelease-news = []\n' + ) + member = tmp_path / "meta" + member.mkdir() + (member / "pyproject.toml").write_text( + f'[project]\nname = "meta"\nrequires-python = ">=3.15"\n[tool.uv.sources]\n{source}\n' + ) + lock = tmp_path / "pylock.toml" + lock.write_text( + 'lock-version = "1.0"\ncreated-by = "release-creator"\nrequires-python = ">=3.15"\n' + 'dependency-groups = ["project-runtime", "release-news"]\n' + "default-groups = []\npackages = []\n[tool.release-creator]\ncreated-at = 2026-10-20T00:00:00Z\n" + ) + tools = Path(__file__).resolve().parents[2] / "tools" + + for command in ( + ["python3", str(tools / "lock_groups.py"), str(lock)], + [ + sys.executable, + str(tools / "check_lock_groups.py"), + str(lock), + "release-news", + "--project", + str(tmp_path / "pyproject.toml"), + ], + ): + result = subprocess.run(command, capture_output=True, text=True, check=False) + assert result.returncode != 0 + assert "uv source must name a workspace project" in result.stderr + + +@pytest.mark.parametrize( + ("groups", "extras", "message"), + [ + pytest.param('["project-runtime", "release-news"]', '["web"]', "omits dependency groups", id="group"), + pytest.param('["project-runtime", "release-news", "test"]', "[]", "omits extras", id="extra"), + ], +) +def test_lock_groups_rejects_incomplete_project_lock(tmp_path: Path, groups: str, extras: str, message: str) -> None: + (tmp_path / "pyproject.toml").write_text( + "[project.optional-dependencies]\nweb = []\n[dependency-groups]\nrelease-news = []\ntest = []\n" + ) + lock = tmp_path / "pylock.toml" + lock.write_text(f"dependency-groups = {groups}\nextras = {extras}\ndefault-groups = []\n[tool.release-creator]\n") + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert message in result.stderr + + +@pytest.mark.parametrize( + ("extra_group", "message"), + [ + pytest.param("release-x\\nchanged=false", "invalid dependency groups", id="output-injection"), + pytest.param("release-other", "invalid or undeclared", id="undeclared"), + pytest.param("release-news", "invalid or undeclared", id="duplicate"), + ], +) +def test_lock_groups_rejects_extra_full_lock_group(tmp_path: Path, extra_group: str, message: str) -> None: + (tmp_path / "pyproject.toml").write_text("[dependency-groups]\nrelease-news = []\n") + lock = tmp_path / "pylock.toml" + lock.write_text( + f'dependency-groups = ["project-runtime", "release-news", "{extra_group}"]\n' + "extras = []\ndefault-groups = []\n[tool.release-creator]\n" + ) + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert message in result.stderr + + +@pytest.mark.parametrize( + "default_groups", + [pytest.param("[]", id="empty"), pytest.param('["test"]', id="test"), pytest.param(None, id="missing")], +) +def test_lock_groups_requires_empty_defaults(tmp_path: Path, default_groups: str | None) -> None: + (tmp_path / "pyproject.toml").write_text("[dependency-groups]\nrelease-news = []\ntest = []\n") + lock = tmp_path / "pylock.toml" + defaults = f"default-groups = {default_groups}\n" if default_groups is not None else "" + lock.write_text( + 'dependency-groups = ["project-runtime", "release-news", "test"]\n' + f"{defaults}[tool.release-creator]\ncreated-at = 2026-10-24T00:00:00Z\n" + ) + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert (result.returncode == 0) is (default_groups == "[]") + + +def test_lock_groups_accepts_project_without_dependency_groups(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[project]\nname = "example"\nversion = "1"\n') + lock = tmp_path / "pylock.toml" + lock.write_text( + 'dependency-groups = ["project-runtime"]\nextras = []\ndefault-groups = []\n[tool.release-creator]\n' + ) + + result = subprocess.run( + ["python3", str(Path(__file__).resolve().parents[2] / "tools/lock_groups.py"), str(lock)], + capture_output=True, + text=True, + check=False, + ) + + assert (result.returncode, result.stdout) == (0, "\n") + + @pytest.mark.parametrize( ("groups", "message"), [ diff --git a/tests/validate_news/test_news.py b/tests/validate_news/test_news.py index d5a45ca..5818376 100644 --- a/tests/validate_news/test_news.py +++ b/tests/validate_news/test_news.py @@ -146,6 +146,7 @@ def test_validate_news_forwards_secondary_lock_inputs() -> None: pytest.param("pylock.zipapp.toml", "pylock.zipapp.toml", "true", id="selected-secondary-lock"), pytest.param("", "pylock.zipapp.toml", "false", id="unselected-secondary-lock"), pytest.param("", "nab.toml", "true", id="nab-policy"), + pytest.param("", "meta/pyproject.toml", "true", id="workspace-member"), ], ) def test_validate_news_detects_release_input_change( @@ -162,6 +163,7 @@ def test_validate_news_detects_release_input_change( run(repository, "git", "commit", "-m", "Add release locks") run(repository, "git", "push", "origin", "main") run(repository, "git", "checkout", "-b", "change-secondary-lock") + (repository / changed_path).parent.mkdir(parents=True, exist_ok=True) (repository / changed_path).write_text( "[tool.nab]\nuploaded-prior-to = false\n" if changed_path == "nab.toml" diff --git a/tests/verify_pypi_files/test_verify.py b/tests/verify_pypi_files/test_verify.py index 96d25cf..3ffbd25 100644 --- a/tests/verify_pypi_files/test_verify.py +++ b/tests/verify_pypi_files/test_verify.py @@ -352,7 +352,7 @@ def test_verify_files_retries_transient_listing_failure( def test_verify_files_retries_response_read_failure( distribution: Path, mocker: MockerFixture, failure: TimeoutError | IncompleteRead ) -> None: - response = mocker.MagicMock() + response = mocker.create_autospec(io.BytesIO, instance=True) response.__enter__.return_value.read.side_effect = failure mocker.patch.object(_MODULE, "urlopen", side_effect=[response, _body(_listing(distribution))]) mocker.patch.object(_MODULE.time, "sleep") diff --git a/tools/build_constraints.py b/tools/build_constraints.py index 7377e9a..3baf3ab 100644 --- a/tools/build_constraints.py +++ b/tools/build_constraints.py @@ -108,10 +108,8 @@ def _selected_build( selected: dict[str, tuple[Version, set[str]]] = {} for index, lock in enumerate(locks): if "build" not in (lock.dependency_groups or ()): - if len(locks) == 2: + if (lock.dependency_groups is not None and len(lock.dependency_groups) > 0) or len(locks) == 2: continue - if lock.dependency_groups: - raise ValueError("custom lock has dependency groups but no build group") groups: set[str] = set() else: groups = {"build"} @@ -122,7 +120,7 @@ def _selected_build( { tag for package in lock.packages - for wheel in package.wheels or () + for wheel in (package.wheels if package.wheels is not None else ()) for tag in parse_wheel_filename(wheel.filename)[3] }, key=str, diff --git a/tools/check_frozen_hooks.py b/tools/check_frozen_hooks.py new file mode 100644 index 0000000..26a3995 --- /dev/null +++ b/tools/check_frozen_hooks.py @@ -0,0 +1,43 @@ +"""Require immutable revisions for external pre-commit hooks.""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + +from yaml import YAMLError, safe_load + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("config", type=Path) + args = parser.parse_args() + try: + check_frozen(args.config) + except (OSError, TypeError, ValueError, YAMLError) as error: + parser.error(str(error)) + + +def check_frozen(path: Path) -> None: + config = safe_load(path.read_text(encoding="utf-8")) + if not isinstance(config, dict) or not isinstance(config.get("repos"), list): + raise TypeError("hook configuration must contain a repository list") + for repository in config["repos"]: + if not isinstance(repository, dict) or not isinstance(repository.get("repo"), str): + raise TypeError("hook repository must be a mapping with a URL") + if repository["repo"] in {"local", "meta"}: + continue + revision = repository.get("rev") + if not isinstance(revision, str) or re.fullmatch(r"[0-9a-f]{40}", revision) is None: + raise ValueError(f"external hook revision must be a full lowercase commit SHA: {repository['repo']}") + + +if __name__ == "__main__": + main() + + +__all__ = [ + "check_frozen", + "main", +] diff --git a/tools/check_hook_config.py b/tools/check_hook_config.py index bb6d131..f56e45f 100644 --- a/tools/check_hook_config.py +++ b/tools/check_hook_config.py @@ -1,4 +1,4 @@ -"""Check the hook changes allowed in a bot pull request.""" +"""Check the hook changes allowed in a pull request.""" from __future__ import annotations @@ -6,6 +6,7 @@ import json import re import ssl +from collections import Counter from collections.abc import Iterator from copy import deepcopy from datetime import UTC, datetime, timedelta @@ -37,15 +38,16 @@ def main() -> None: parser.add_argument("base", type=Path) parser.add_argument("proposed", type=Path) parser.add_argument("--managed", choices=("true", "false", ""), default="") + parser.add_argument("--human", choices=("true", "false", ""), default="") parser.add_argument("--minimum-days", type=int, default=7) args = parser.parse_args() try: - check_config(args.base, args.proposed, args.managed == "true", args.minimum_days) + check_config(args.base, args.proposed, args.managed == "true", args.minimum_days, human=args.human == "true") except (InvalidRequirement, KeyError, OSError, TypeError, ValueError, YAMLError) as error: parser.error(str(error)) -def check_config(base: Path, proposed: Path, managed: bool, minimum_days: int) -> None: +def check_config(base: Path, proposed: Path, managed: bool, minimum_days: int, *, human: bool = False) -> None: if minimum_days < 7: raise ValueError("hook cooldown must be at least seven days") base_source = base.read_text(encoding="utf-8") @@ -56,6 +58,14 @@ def check_config(base: Path, proposed: Path, managed: bool, minimum_days: int) - new = safe_load(proposed_source) if not isinstance(old, dict) or not isinstance(new, dict): raise TypeError("hook configuration must be a mapping") + if human: + if not isinstance(old.get("repos"), list) or not isinstance(new.get("repos"), list): + raise TypeError("hook repositories must be lists") + if Counter(item for group in _dependency_groups(base_source) for item, managed in group if managed) != Counter( + item for group in _dependency_groups(proposed_source) for item, managed in group if managed + ): + raise ValueError("managed hook dependencies require the release App") + return old_content = deepcopy(old) new_content = deepcopy(new) old_repos = old_content.get("repos") @@ -152,22 +162,24 @@ def _check_dependency_group(old: list[tuple[str, bool]], new: list[tuple[str, bo ): raise ValueError("hook update changes direct dependencies beyond an exact pin") _check_pypi(new_requirement, minimum_days) - previous_managed = {_requirement_key(Requirement(item)) for item, marked in old if marked} - proposed_managed = {_requirement_key(Requirement(item)) for item, marked in new if marked} - if previous_managed - proposed_managed: - raise ValueError("hook update removes managed dependencies") - names = [canonicalize_name(Requirement(item).name) for item, _ in new] - if len(names) != len(set(names)): + direct_requirements = [Requirement(item) for item in new_direct] + direct_names = [canonicalize_name(requirement.name) for requirement in direct_requirements] + managed_names = [canonicalize_name(Requirement(item).name) for item, marked in new if marked] + managed_set = set(managed_names) + if ( + len(direct_names) != len(set(direct_names)) + or len(managed_names) != len(managed_set) + or any( + canonicalize_name(requirement.name) in managed_set and _exact(requirement) + for requirement in direct_requirements + ) + ): raise ValueError("hook update duplicates a dependency") for item, marked in new: if marked and item not in {previous for previous, old_marked in old if old_marked}: _check_pypi(Requirement(item), minimum_days) -def _requirement_key(requirement: Requirement) -> tuple[str, frozenset[str], str]: - return canonicalize_name(requirement.name), frozenset(requirement.extras), str(requirement.marker) - - def _exact(requirement: Requirement) -> bool: return ( requirement.url is None diff --git a/tools/check_lock_groups.py b/tools/check_lock_groups.py index 492fb95..9568547 100644 --- a/tools/check_lock_groups.py +++ b/tools/check_lock_groups.py @@ -8,7 +8,7 @@ import time import tomllib from collections import defaultdict -from collections.abc import Iterator +from collections.abc import Iterable, Iterator from concurrent.futures import ThreadPoolExecutor from datetime import UTC, datetime, timedelta from http.client import IncompleteRead @@ -19,11 +19,12 @@ from urllib.request import Request, urlopen import certifi +from lock_groups import lock_provider, project_selections from nab_cutoffs import reviewed_cutoff, validate_indexes from packaging._parser import Variable from packaging.markers import Marker from packaging.pylock import Package, Pylock, PylockValidationError -from packaging.utils import canonicalize_name, parse_sdist_filename, parse_wheel_filename +from packaging.utils import canonicalize_name from packaging.version import InvalidVersion, Version @@ -47,12 +48,16 @@ def main() -> None: if (args.project.parent / "nab.toml").exists(): raise ValueError("project nab.toml can override the reviewed index policy") with args.project.open("rb") as stream: - validate_indexes(tomllib.load(stream)["tool"]["nab"]) + project = tomllib.load(stream) + if "nab" in project.get("tool", {}): + validate_indexes(project["tool"]["nab"]) + if lock_provider(lock.tool) == "release-creator": + project_selections(args.project) if (missing := groups - set(lock.dependency_groups or ())) and not ( args.build_constraints and missing == {"build"} ): parser.error(f"missing lock dependency groups: {', '.join(sorted(missing))}") - if args.build_constraints and not lock.dependency_groups: + if args.build_constraints and "build" not in (lock.dependency_groups or ()): groups.clear() _check_cooling(lock, groups, local_tool_source=args.local_tool_source) if args.verify_pypi: @@ -70,10 +75,13 @@ def main() -> None: def _check_cooling(lock: Pylock, groups: set[str], *, local_tool_source: bool = False) -> None: - created = lock.tool.get("nab", {}).get("created-at") if lock.tool else None + created = ( + (lock.tool or {}).get("nab" if lock_provider(lock.tool) == "nab" else "release-creator", {}).get("created-at") + ) if not isinstance(created, datetime) or created.tzinfo is None or created > datetime.now(tz=UTC): raise ValueError("lock has no valid creation time or was created in the future") threshold = created - timedelta(days=7) + whole_seconds = lock.created_by == "release-creator (uv)" and lock_provider(lock.tool) == "release-creator" for package in _selected_packages(lock, groups, local_tool_source=local_tool_source): if package.archive is not None or package.vcs is not None or package.directory is not None: raise ValueError(f"release lock has a direct-source package: {package.name}") @@ -87,16 +95,10 @@ def _check_cooling(lock: Pylock, groups: set[str], *, local_tool_source: bool = url = urlsplit(artifact.url or "") if artifact.path is not None or url.scheme != "https" or url.netloc != "files.pythonhosted.org": raise ValueError(f"release lock has a non-PyPI artifact: {artifact.name}") - filename = url.path.rsplit("/", 1)[-1] - identity = ( - parse_wheel_filename(filename)[:2] if filename.endswith(".whl") else parse_sdist_filename(filename) - ) - if identity != (canonicalize_name(package.name), Version(str(package.version))): - raise ValueError(f"release lock artifact differs from package version: {filename}") if ( artifact.upload_time is None or artifact.upload_time.tzinfo is None - or artifact.upload_time > min(cutoff, created) + or artifact.upload_time + timedelta(seconds=whole_seconds) > min(cutoff, created) ): raise ValueError(f"release lock has an uncooled artifact: {artifact.name}") @@ -113,20 +115,32 @@ def _verify_pypi(lock: Pylock, groups: set[str], *, local_tool_source: bool) -> artifact.upload_time, ) ) + whole_seconds = lock.created_by == "release-creator (uv)" and lock_provider(lock.tool) == "release-creator" with ThreadPoolExecutor(max_workers=8) as pool: - for _ in pool.map(_verify_package_metadata, expected.items()): + for _ in pool.map( + _verify_package_metadata, ((name, artifacts, whole_seconds) for name, artifacts in expected.items()) + ): pass -def _verify_package_metadata(item: tuple[str, list[tuple[str, str | None, str, datetime | None]]]) -> None: - name, artifacts = item +def _verify_package_metadata(item: tuple[str, list[tuple[str, str | None, str, datetime | None]], bool]) -> None: + name, artifacts, whole_seconds = item for attempt in range(5): _, actual = _pypi_files(name) mismatch = next( ( filename for filename, url, digest, upload_time in artifacts - if actual.get(filename) != (url, digest, upload_time) + if not ( + (record := actual.get(filename)) is not None + and record[:2] == (url, digest) + and ( + record[2] == upload_time + or whole_seconds + and record[2] is not None + and record[2].replace(microsecond=0) == upload_time + ) + ) ), None, ) @@ -153,14 +167,14 @@ def _selected_packages(lock: Pylock, groups: set[str], *, local_tool_source: boo and package.archive is None and package.vcs is None and package.sdist is None - and not package.wheels + and (package.wheels is None or len(package.wheels) == 0) ): continue yield package def _possibly_selected(marker: Marker, groups: set[str]) -> bool: - def expression(nodes: list[object]) -> bool: + def expression(nodes: Iterable[object]) -> bool: alternatives: list[bool] = [] current = True for node in nodes: @@ -178,6 +192,8 @@ def expression(nodes: list[object]) -> bool: isinstance(right, Variable) and str(right) == "dependency_groups" and str(operator) == "not in" ): current &= str(left) not in groups + elif isinstance(right, Variable) and str(right) == "extras": + current &= str(operator) == "not in" return any((*alternatives, current)) return expression(marker._markers) diff --git a/tools/check_recent_cutoffs.py b/tools/check_recent_cutoffs.py index b9b816d..6da4ee1 100644 --- a/tools/check_recent_cutoffs.py +++ b/tools/check_recent_cutoffs.py @@ -1,12 +1,13 @@ -"""Reject recent Nab cutoffs without an explicit reviewed exception.""" +"""Reject recent package cutoffs without an explicit reviewed exception.""" from __future__ import annotations import argparse +import tomllib from datetime import UTC, datetime, timedelta from pathlib import Path -from nab_cutoffs import validate_recent_cutoffs +from nab_cutoffs import validate_recent_cutoffs, validate_release_creator_cutoffs def main() -> None: @@ -16,7 +17,15 @@ def main() -> None: try: if (args.project.parent / "nab.toml").exists(): parser.error("project nab.toml can override the reviewed cutoff policy") - validate_recent_cutoffs(args.project.read_text(), datetime.now(UTC) - timedelta(days=7)) + project = args.project.read_text() + threshold = datetime.now(UTC) - timedelta(days=7) + tool = tomllib.loads(project)["tool"] + if "nab" in tool: + validate_recent_cutoffs(project, threshold) + if "release-creator" in tool: + validate_release_creator_cutoffs(project, threshold) + if "nab" not in tool and "release-creator" not in tool: + raise ValueError("project has no lock cutoff policy") except (OSError, KeyError, TypeError, ValueError) as error: parser.error(str(error)) diff --git a/tools/expire_cutoffs.py b/tools/expire_cutoffs.py index 3ed9857..d6e30cb 100644 --- a/tools/expire_cutoffs.py +++ b/tools/expire_cutoffs.py @@ -1,12 +1,13 @@ -"""Remove reviewed Nab cutoffs once the global seven-day window covers them.""" +"""Remove reviewed package cutoffs once the global seven-day window covers them.""" from __future__ import annotations import argparse +import tomllib from datetime import UTC, datetime, timedelta from pathlib import Path -from nab_cutoffs import remove_expired_cutoffs +from nab_cutoffs import remove_expired_cutoffs, remove_expired_release_creator_cutoffs from tomlkit.exceptions import ParseError @@ -16,7 +17,11 @@ def main() -> None: args = parser.parse_args() try: source = args.project.read_text() - updated = remove_expired_cutoffs(source, datetime.now(UTC) - timedelta(days=7)) + threshold = datetime.now(UTC) - timedelta(days=7) + tool = tomllib.loads(source)["tool"] + updated = remove_expired_cutoffs(source, threshold) if "nab" in tool else source + if "release-creator" in tool: + updated = remove_expired_release_creator_cutoffs(updated, threshold) if source != updated: args.project.write_text(updated) except (OSError, KeyError, TypeError, ValueError, ParseError) as error: diff --git a/tools/lock_groups.py b/tools/lock_groups.py index fbcf1d6..bdec063 100644 --- a/tools/lock_groups.py +++ b/tools/lock_groups.py @@ -1,19 +1,48 @@ -"""Print the dependency groups used to create a Nab lock.""" +"""Read the lock producer and its selected release groups.""" from __future__ import annotations import argparse import tomllib +from collections.abc import Mapping from pathlib import Path def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("lock", type=Path) + parser.add_argument("--provider", action="store_true") args = parser.parse_args() try: with args.lock.open("rb") as stream: lock = tomllib.load(stream) + provider = lock_provider(lock.get("tool")) + if args.provider: + print(provider) + return + if provider == "release-creator": + declared, declared_extras = project_selections(args.lock.with_name("pyproject.toml")) + if lock.get("default-groups") != []: + raise ValueError("project lock must not install default groups") + selected = lock["dependency-groups"] + if not isinstance(selected, list) or not all(isinstance(group, str) for group in selected): + raise ValueError("project lock has invalid dependency groups") + if not all(group.isascii() and group.replace("-", "").isalnum() for group in selected): + raise ValueError("project lock has invalid dependency groups") + required = declared | {"project-runtime"} + if missing := required - set(selected): + raise ValueError(f"project lock omits dependency groups: {', '.join(sorted(missing))}") + if len(selected) != len(set(selected)) or set(selected) - required - {"build"}: + raise ValueError("project lock has invalid or undeclared dependency groups") + extras = lock.get("extras", []) + if not isinstance(extras, list) or not all(isinstance(extra, str) for extra in extras): + raise ValueError("project lock has invalid extras") + if missing_extras := declared_extras - set(extras): + raise ValueError(f"project lock omits extras: {', '.join(sorted(missing_extras))}") + if len(extras) != len(set(extras)) or set(extras) != declared_extras: + raise ValueError("project lock has invalid or undeclared extras") + print(" ".join(group for group in selected if group.startswith("release-"))) + return command = lock["tool"]["nab"]["command-line"] if not isinstance(command, list) or not all(isinstance(value, str) for value in command): raise ValueError("Nab command-line must be a list of strings") @@ -26,7 +55,7 @@ def main() -> None: if not groups: parser.error("Nab lock records no selected groups") with args.lock.with_name("pyproject.toml").open("rb") as stream: - declared = tomllib.load(stream)["dependency-groups"] + declared = tomllib.load(stream).get("dependency-groups", {}) if not all(group.isascii() and group.replace("-", "").isalnum() and group in declared for group in groups): raise ValueError("Nab lock selects an invalid or undeclared group") required = {name for name in declared if name.startswith("release-")} @@ -34,8 +63,64 @@ def main() -> None: raise ValueError(f"Nab lock omits release groups: {', '.join(sorted(missing))}") print(" ".join(groups)) except (KeyError, OSError, TypeError, ValueError) as error: - parser.error(f"cannot read Nab lock groups: {error}") + parser.error(f"cannot read lock groups: {error}") + + +def project_selections(project: Path) -> tuple[set[str], set[str]]: + with project.open("rb") as stream: + root = tomllib.load(stream) + workspace = root.get("tool", {}).get("uv", {}).get("workspace", {}) + members = workspace.get("members", []) + excluded = workspace.get("exclude", []) + if not all( + isinstance(patterns, list) and all(isinstance(pattern, str) for pattern in patterns) + for patterns in (members, excluded) + ): + raise ValueError("workspace patterns must be strings") + directory = project.parent.resolve() + + def matched(patterns: list[str]) -> set[Path]: + paths = {path.resolve() for pattern in patterns for path in directory.glob(pattern)} + if any(not path.is_relative_to(directory) for path in paths): + raise ValueError("workspace members must be inside the project") + return paths + + projects = [root] + for member in sorted(matched(members) - matched(excluded)): + if member.is_dir(): + with (member / "pyproject.toml").open("rb") as stream: + projects.append(tomllib.load(stream)) + names = { + _normalized_name(name) for item in projects if isinstance(name := item.get("project", {}).get("name"), str) + } + for item in projects: + for name, source in item.get("tool", {}).get("uv", {}).get("sources", {}).items(): + if source != {"workspace": True} or _normalized_name(name) not in names: + raise ValueError(f"uv source must name a workspace project: {name}") + return ( + {group for item in projects for group in item.get("dependency-groups", {})}, + {extra for item in projects for extra in item.get("project", {}).get("optional-dependencies", {})}, + ) + + +def _normalized_name(name: str) -> str: + return "-".join(filter(None, name.casefold().replace("_", "-").replace(".", "-").split("-"))) + + +def lock_provider(tool: Mapping[str, object] | None) -> str: + if "release-creator" in (tool or {}): + return "release-creator" + if "nab" in (tool or {}): + return "nab" + raise ValueError("lock has no recognized producer") if __name__ == "__main__": main() + + +__all__ = [ + "lock_provider", + "main", + "project_selections", +] diff --git a/tools/nab_cutoffs.py b/tools/nab_cutoffs.py index 3448e3d..b5ef527 100644 --- a/tools/nab_cutoffs.py +++ b/tools/nab_cutoffs.py @@ -24,6 +24,7 @@ ("release-creator", "0.6.6"): datetime(2026, 10, 23, 20, 48, 24, tzinfo=UTC), ("release-creator", "0.6.7"): datetime(2026, 10, 23, 21, 40, 6, tzinfo=UTC), ("release-creator", "1.0.0"): datetime(2026, 10, 24, 6, 7, 46, tzinfo=UTC), + ("release-creator", "1.1.0"): datetime(2026, 10, 24, 9, 8, 40, tzinfo=UTC), ("uv-build", "0.13.4"): datetime(2026, 10, 21, tzinfo=UTC), } _REVIEWED_EXCEPTIONS: Final = { @@ -65,17 +66,30 @@ def validate_recent_cutoffs(project: str, threshold: datetime) -> None: if int(match["days"]) < _MINIMUM_DAYS: raise ValueError(f"Nab cutoff for {name} weakens the seven-day policy: {value}") continue - if isinstance(value, bool): - raise TypeError(f"invalid Nab cutoff for {name}: {value}") - timestamp = datetime.fromisoformat(value) if isinstance(value, str) else value - if timestamp.tzinfo is None: - raise ValueError(f"Nab package cutoff has no timezone: {value}") + timestamp = _timestamp(name, value) if timestamp > threshold and timestamp not in { reviewed for (package, _), reviewed in _REVIEWED_VERSIONS.items() if package == name }: raise ValueError(f"unreviewed recent Nab cutoff for {name}: {timestamp.isoformat()}") +def validate_release_creator_cutoffs(project: str, threshold: datetime) -> None: + config = tomllib.loads(project)["tool"]["release-creator"].get("locks", {}) + if set(config) - {"exclude-newer-package"}: + raise ValueError("release-creator lock policy must contain only exclude-newer-package") + overrides = config.get("exclude-newer-package", {}) + if not isinstance(overrides, dict): + raise TypeError("release-creator package cutoffs must be a table") + for name, value in overrides.items(): + if not isinstance(value, str): + raise TypeError(f"invalid release-creator cutoff for {name}: {value}") + timestamp = _timestamp(name, value) + if timestamp > threshold and timestamp not in { + reviewed for (package, _), reviewed in _REVIEWED_VERSIONS.items() if package == name + }: + raise ValueError(f"unreviewed recent release-creator cutoff for {name}: {timestamp.isoformat()}") + + def validate_indexes(nab: dict) -> None: if any(index != {"name": "pypi", "url": "https://pypi.org/simple/"} for index in nab.get("indexes", [])): raise ValueError("custom Nab indexes are not approved for release locks") @@ -96,6 +110,36 @@ def remove_expired_cutoffs(project: str, threshold: datetime) -> str: return tomlkit.dumps(document) +def remove_expired_release_creator_cutoffs(project: str, threshold: datetime) -> str: + import tomlkit + + names = expired_release_creator_cutoffs(project, threshold) + if not names: + return project + document = tomlkit.parse(project) + remove_release_creator_cutoffs(document["tool"]["release-creator"]["locks"], names) + return tomlkit.dumps(document) + + +def remove_release_creator_cutoffs(locks: dict, names: set[str]) -> None: + for name in names: + del locks["exclude-newer-package"][name] + if not locks["exclude-newer-package"]: + del locks["exclude-newer-package"] + + +def expired_release_creator_cutoffs(project: str, threshold: datetime) -> set[str]: + overrides = tomllib.loads(project)["tool"]["release-creator"].get("locks", {}).get("exclude-newer-package", {}) + return { + name + for name, value in overrides.items() + if name in _REVIEWED_EXCEPTIONS + and _timestamp(name, value) + in {reviewed for (package, _), reviewed in _REVIEWED_VERSIONS.items() if package == name} + and _timestamp(name, value) <= threshold + } + + def remove_cutoffs(nab: dict, names: set[str], positions: set[int]) -> None: for name in names: settings = nab["packages"][name] @@ -142,19 +186,28 @@ def expired_cutoffs(project: str, threshold: datetime) -> tuple[set[str], set[in def _expired(value: str | datetime | bool, threshold: datetime, reviewed: set[datetime]) -> bool: if value is False or isinstance(value, str) and _DURATION.fullmatch(value) is not None: return False - if isinstance(value, bool): - raise TypeError(f"invalid Nab cutoff: {value}") + timestamp = _timestamp("Nab", value) + return timestamp in reviewed and timestamp <= threshold + + +def _timestamp(name: str, value: str | datetime | bool) -> datetime: + if not isinstance(value, str | datetime) or isinstance(value, bool): + raise TypeError(f"invalid cutoff for {name}: {value}") timestamp = datetime.fromisoformat(value) if isinstance(value, str) else value if timestamp.tzinfo is None: - raise ValueError(f"Nab package cutoff has no timezone: {value}") - return timestamp in reviewed and timestamp <= threshold + raise ValueError(f"cutoff for {name} has no timezone: {value}") + return timestamp __all__ = [ "expired_cutoffs", + "expired_release_creator_cutoffs", "remove_cutoffs", "remove_expired_cutoffs", + "remove_expired_release_creator_cutoffs", + "remove_release_creator_cutoffs", "reviewed_cutoff", "validate_indexes", "validate_recent_cutoffs", + "validate_release_creator_cutoffs", ] diff --git a/tools/verify_cutoff_patch.py b/tools/verify_cutoff_patch.py index 1b6c525..ee0d554 100644 --- a/tools/verify_cutoff_patch.py +++ b/tools/verify_cutoff_patch.py @@ -1,4 +1,4 @@ -"""Allow only expired Nab upload cutoffs to change in pyproject.toml.""" +"""Allow only expired package cutoffs to change in pyproject.toml.""" from __future__ import annotations @@ -8,7 +8,7 @@ from datetime import UTC, datetime, timedelta from pathlib import Path -from nab_cutoffs import expired_cutoffs, remove_cutoffs +from nab_cutoffs import expired_cutoffs, expired_release_creator_cutoffs, remove_cutoffs, remove_release_creator_cutoffs def main() -> None: @@ -24,18 +24,28 @@ def main() -> None: ).stdout original = tomllib.loads(baseline) actual = tomllib.loads(args.project.read_text()) - names, positions = expired_cutoffs(baseline, datetime.now(UTC) - timedelta(days=7)) - before = original["tool"]["nab"] - after = actual["tool"]["nab"] - removed_names = { - name for name in names if after.get("packages", {}).get(name, {}).get("uploaded-prior-to") is None - } - removed_positions = { - index for index in positions if before["package-rules"][index] not in after.get("package-rules", []) - } - remove_cutoffs(before, removed_names, removed_positions) + threshold = datetime.now(UTC) - timedelta(days=7) + if "nab" in original["tool"]: + names, positions = expired_cutoffs(baseline, threshold) + before = original["tool"]["nab"] + after = actual["tool"]["nab"] + removed_names = { + name for name in names if after.get("packages", {}).get(name, {}).get("uploaded-prior-to") is None + } + removed_positions = { + index for index in positions if before["package-rules"][index] not in after.get("package-rules", []) + } + remove_cutoffs(before, removed_names, removed_positions) + if "release-creator" in original["tool"]: + names = expired_release_creator_cutoffs(baseline, threshold) + before = original["tool"]["release-creator"].get("locks", {}) + after = actual["tool"]["release-creator"].get("locks", {}) + removed_names = {name for name in names if name not in after.get("exclude-newer-package", {})} + if removed_names: + remove_release_creator_cutoffs(before, removed_names) if original != actual: - parser.error("pyproject.toml has changes beyond expired Nab cutoffs") + producer = "package" if "release-creator" in original["tool"] else "Nab" + parser.error(f"pyproject.toml has changes beyond expired {producer} cutoffs") except (OSError, KeyError, TypeError, ValueError, subprocess.CalledProcessError, tomllib.TOMLDecodeError) as error: parser.error(str(error)) diff --git a/validate-news/action.yml b/validate-news/action.yml index 3b3f137..decd30c 100644 --- a/validate-news/action.yml +++ b/validate-news/action.yml @@ -45,7 +45,7 @@ runs: BASE_REF: ${{ github.base_ref }} ADDITIONAL_LOCK: ${{ inputs.additional-lock }} run: | - paths=(pyproject.toml pylock.toml nab.toml) + paths=(pyproject.toml ':(glob)**/pyproject.toml' pylock.toml nab.toml) if [[ -n "$ADDITIONAL_LOCK" ]]; then paths+=("$ADDITIONAL_LOCK"); fi changed=$(git diff --name-only "origin/$BASE_REF...HEAD" -- "${paths[@]}") if [[ -z "$changed" ]]; then diff --git a/validate-release-patch/action.yml b/validate-release-patch/action.yml index 9031cb1..15e328e 100644 --- a/validate-release-patch/action.yml +++ b/validate-release-patch/action.yml @@ -127,7 +127,14 @@ runs: - name: Run prek on release patch if: inputs.checks == 'true' shell: bash + env: + UV_EXCLUDE_NEWER: 7 days + UV_NO_CONFIG: "1" + UV_NO_SOURCES: "1" run: | + unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL + unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE + unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR set -euo pipefail if ! prek run --all-files; then echo '::error::prek failed on the validated release patch' >&2 diff --git a/verify-frozen-hooks/action.yml b/verify-frozen-hooks/action.yml new file mode 100644 index 0000000..eb01ca0 --- /dev/null +++ b/verify-frozen-hooks/action.yml @@ -0,0 +1,18 @@ +name: Verify frozen hooks +description: Check immutable revisions and the complete cooled Python dependency closure +runs: + using: composite + steps: + - uses: $/install-locked-tools + with: + groups: release-notes + lock-source: shared + venv: ${{ runner.temp }}/frozen-hook-tools + - name: Check external hook revisions + shell: bash + run: | + uv run --no-project --python "$RUNNER_TEMP/frozen-hook-tools" python \ + "$GITHUB_ACTION_PATH/../tools/check_frozen_hooks.py" .pre-commit-config.yaml + - name: Check Python hook dependencies + shell: bash + run: uv run --no-project --python "$RUNNER_TEMP/frozen-hook-tools" release-creator hooks --check From 9e11003e6503341a2d2b110cb6eee8e98b11ef17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sat, 24 Oct 2026 02:29:18 -0700 Subject: [PATCH 3/4] fix(hooks): allow initial managed pin migration A repository with no managed hook pins needs one reviewed PR to add the complete cooled closure. Keep subsequent human changes to managed pins blocked. Set author identity in scratch Git repositories so cutoff tests run on hosted runners. --- README.md | 12 +++--- changelog.d/+hook-closure.bugfix.md | 3 +- .../fixtures/pre-bootstrap.yaml | 40 +++++++++++++++++++ .../test_hook_config.py | 25 ++++++++++++ tests/refresh_release_lock/test_cutoffs.py | 36 +++++++++++++++-- tools/check_hook_config.py | 3 +- 6 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 tests/check_hook_update_cooldown/fixtures/pre-bootstrap.yaml diff --git a/README.md b/README.md index bf51935..2f9eac9 100644 --- a/README.md +++ b/README.md @@ -161,11 +161,13 @@ proposed configuration from that exact head, including a fork head. It checks th closure at the configured `minimum-days` cutoff and requires each changed revision to have appeared in a GitHub PR workflow run at least that many days earlier. The default is seven days. A force push does not reset an unchanged revision's age or give a new revision the old one's age. Human PRs may add hooks or change their options under the same -revision and dependency checks, but only the release App may change managed dependency pins. Other bot PRs may change -revisions only. On `automation/update-release-tools`, the Action verifies the bot against `RELEASE_APP_CLIENT_ID` and -permits exact, cooled PyPI dependency pins generated by the release App. Every external hook revision must be a full -lowercase commit SHA. A revision with new Python dependencies stays blocked until the App updates its managed pins. -Tagged source jobs check frozen revisions and the complete Python dependency closure before running `prek`. +revision and dependency checks. A human PR may add the first managed pins when the base configuration has none; the +complete closure check still applies. Once the base has managed pins, only the release App may change them. Other bot +PRs may change revisions only. On `automation/update-release-tools`, the Action verifies the bot against +`RELEASE_APP_CLIENT_ID` and permits exact, cooled PyPI dependency pins generated by the release App. Every external hook +revision must be a full lowercase commit SHA. A revision with new Python dependencies stays blocked until the App +updates its managed pins. Tagged source jobs check frozen revisions and the complete Python dependency closure before +running `prek`. ```yaml hook-cooldown: diff --git a/changelog.d/+hook-closure.bugfix.md b/changelog.d/+hook-closure.bugfix.md index a41057a..f221bb8 100644 --- a/changelog.d/+hook-closure.bugfix.md +++ b/changelog.d/+hook-closure.bugfix.md @@ -1,3 +1,4 @@ Check the cooldown, immutable hook revisions, and complete Python hook dependency pins on every pull request that changes the hook configuration, including human and fork pull requests. Check the hook pins again before running hooks -from a release tag. Run hook installs from PyPI with the seven-day cutoff, ignoring repository uv configuration. +from a release tag. Allow the first complete set of managed pins in a human PR while keeping later pin changes limited +to the release App. Run hook installs from PyPI with the seven-day cutoff, ignoring repository uv configuration. diff --git a/tests/check_hook_update_cooldown/fixtures/pre-bootstrap.yaml b/tests/check_hook_update_cooldown/fixtures/pre-bootstrap.yaml new file mode 100644 index 0000000..0810075 --- /dev/null +++ b/tests/check_hook_update_cooldown/fixtures/pre-bootstrap.yaml @@ -0,0 +1,40 @@ +default_language_version: + python: python3.15 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 38bfb07c0c64c304f47eb52090f8288150c77730 # v0.17.1 + hooks: + - id: ruff-check + args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] + - id: ruff-format + types_or: [python, pyi] + - repo: local + hooks: + - id: mdformat + name: mdformat + entry: mdformat --wrap 120 + language: python + types: [markdown] + additional_dependencies: [mdformat-gfm==1.0.0] + - repo: https://github.com/scop/pre-commit-shfmt + rev: 479be5958357ba5ab65ce47172117348516860e9 # v3.14.1-1 + hooks: + - id: shfmt + args: ["-i", "2", "-d"] + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: 745eface02aef23e168a8afb6b5737818efbea95 # v0.11.0.1 + hooks: + - id: shellcheck + - repo: https://github.com/google/yamlfmt + rev: 21ca5323a9c87ee37a434e0ca908efc0a89daa07 # v0.21.0 + hooks: + - id: yamlfmt + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: c92afdf2c6ca336aaa8f39e2bd8d48f62dbd8e1f # v1.31.1 + hooks: + - id: zizmor diff --git a/tests/check_hook_update_cooldown/test_hook_config.py b/tests/check_hook_update_cooldown/test_hook_config.py index b43da45..875ede7 100644 --- a/tests/check_hook_update_cooldown/test_hook_config.py +++ b/tests/check_hook_update_cooldown/test_hook_config.py @@ -2,6 +2,8 @@ import json import runpy +import subprocess +import sys from collections.abc import Callable from datetime import UTC, datetime, timedelta from io import BytesIO @@ -202,6 +204,29 @@ def test_human_cannot_change_managed_pin(configs: tuple[Path, Path], check_confi check_config(base, proposed, False, 7, human=True) +def test_human_can_bootstrap_managed_pins_from_release_migration() -> None: + root = Path(__file__).resolve().parents[2] + result = subprocess.run( + [ + sys.executable, + str(root / "tools/check_hook_config.py"), + str(Path(__file__).parent / "fixtures/pre-bootstrap.yaml"), + str(root / ".pre-commit-config.yaml"), + "--managed", + "", + "--human", + "true", + "--minimum-days", + "7", + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + + def test_app_update_keeps_direct_range( configs: tuple[Path, Path], check_config: Callable[..., None], pypi_file: dict[str, object], pypi_request: MagicMock ) -> None: diff --git a/tests/refresh_release_lock/test_cutoffs.py b/tests/refresh_release_lock/test_cutoffs.py index dd0c700..6d04c66 100644 --- a/tests/refresh_release_lock/test_cutoffs.py +++ b/tests/refresh_release_lock/test_cutoffs.py @@ -278,7 +278,12 @@ def test_verify_cutoff_patch_rejects_early_floor_removal(cutoff_project: Path) - [pytest.param("release-creator", True, id="reviewed-expired"), pytest.param("bad-package", False, id="hold")], ) def test_verify_cutoff_patch_limits_release_creator_removal( - cutoff_project: Path, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, remove: str, accepted: bool + cutoff_project: Path, + monkeypatch: pytest.MonkeyPatch, + mocker: MockerFixture, + capsys: pytest.CaptureFixture[str], + remove: str, + accepted: bool, ) -> None: cutoff_project.write_text( cutoff_project.read_text() @@ -288,7 +293,19 @@ def test_verify_cutoff_patch_limits_release_creator_removal( ) subprocess.run(["git", "add", "pyproject.toml"], cwd=cutoff_project.parent, check=True, capture_output=True) subprocess.run( - ["git", "commit", "-m", "Add release policy"], cwd=cutoff_project.parent, check=True, capture_output=True + [ + "git", + "-c", + "user.name=Release test", + "-c", + "user.email=release@example.org", + "commit", + "-m", + "Add release policy", + ], + cwd=cutoff_project.parent, + check=True, + capture_output=True, ) cutoff_project.write_text(cutoff_project.read_text().replace(f'{remove} = "', f'# {remove} = "')) script = Path(__file__).resolve().parents[2] / "tools/verify_cutoff_patch.py" @@ -305,6 +322,7 @@ def test_verify_cutoff_patch_limits_release_creator_removal( else: with pytest.raises(SystemExit, match="2"): main() + assert "changes beyond expired package cutoffs" in capsys.readouterr().err def test_verify_cutoff_patch_accepts_release_creator_only_policy( @@ -315,7 +333,19 @@ def test_verify_cutoff_patch_accepts_release_creator_only_policy( ) subprocess.run(["git", "add", "pyproject.toml"], cwd=cutoff_project.parent, check=True, capture_output=True) subprocess.run( - ["git", "commit", "-m", "Add release policy"], cwd=cutoff_project.parent, check=True, capture_output=True + [ + "git", + "-c", + "user.name=Release test", + "-c", + "user.email=release@example.org", + "commit", + "-m", + "Add release policy", + ], + cwd=cutoff_project.parent, + check=True, + capture_output=True, ) cutoff_project.write_text("[tool.release-creator.locks]\n") script = Path(__file__).resolve().parents[2] / "tools/verify_cutoff_patch.py" diff --git a/tools/check_hook_config.py b/tools/check_hook_config.py index f56e45f..e6a5771 100644 --- a/tools/check_hook_config.py +++ b/tools/check_hook_config.py @@ -61,7 +61,8 @@ def check_config(base: Path, proposed: Path, managed: bool, minimum_days: int, * if human: if not isinstance(old.get("repos"), list) or not isinstance(new.get("repos"), list): raise TypeError("hook repositories must be lists") - if Counter(item for group in _dependency_groups(base_source) for item, managed in group if managed) != Counter( + old_managed = Counter(item for group in _dependency_groups(base_source) for item, managed in group if managed) + if old_managed and old_managed != Counter( item for group in _dependency_groups(proposed_source) for item, managed in group if managed ): raise ValueError("managed hook dependencies require the release App") From c4ae3368a12aa293009e22e99e3008e57514bde7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sat, 24 Oct 2026 02:34:34 -0700 Subject: [PATCH 4/4] fix(hooks): bind managed pins to hook IDs A human could move a managed pin to another hook while preserving the global pin count. Compare pins by repository and hook identity, and reject duplicate identities when managed pins exist. --- .../test_hook_config.py | 89 +++++++++++++++++++ tools/check_hook_config.py | 25 +++++- 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/tests/check_hook_update_cooldown/test_hook_config.py b/tests/check_hook_update_cooldown/test_hook_config.py index 875ede7..d4310e5 100644 --- a/tests/check_hook_update_cooldown/test_hook_config.py +++ b/tests/check_hook_update_cooldown/test_hook_config.py @@ -204,6 +204,95 @@ def test_human_cannot_change_managed_pin(configs: tuple[Path, Path], check_confi check_config(base, proposed, False, 7, human=True) +@pytest.mark.parametrize( + ("proposed_hooks", "accepted"), + [ + pytest.param( + " - id: second\n - id: first\n additional_dependencies:\n" + ' - "other==1.2.0" # release-creator managed\n', + True, + id="reorder-same-owner", + ), + pytest.param( + " - id: first\n - id: second\n additional_dependencies:\n" + ' - "other==1.2.0" # release-creator managed\n', + False, + id="relocate-pin", + ), + ], +) +def test_human_managed_pin_stays_with_its_hook( + configs: tuple[Path, Path], + check_config: Callable[..., None], + proposed_hooks: str, + accepted: bool, +) -> None: + base, proposed = configs + prefix = "repos:\n - repo: https://example.org/hook\n rev: old\n hooks:\n" + base.write_text( + prefix + + " - id: first\n additional_dependencies:\n" + + ' - "other==1.2.0" # release-creator managed\n' + + " - id: second\n" + ) + proposed.write_text(prefix + proposed_hooks) + + if accepted: + check_config(base, proposed, False, 7, human=True) + else: + with pytest.raises(ValueError, match="managed hook dependencies require the release App"): + check_config(base, proposed, False, 7, human=True) + + +@pytest.mark.parametrize( + "proposal", + [ + pytest.param( + "repos:\n - repo: local\n hooks:\n" + " - id: a\n name: new\n additional_dependencies:\n" + ' - "other==1.2.0" # release-creator managed\n' + " - id: a\n name: original\n", + id="duplicate-hook", + ), + pytest.param( + "repos:\n - repo: local\n hooks:\n" + " - id: a\n name: new\n additional_dependencies:\n" + ' - "other==1.2.0" # release-creator managed\n' + " - repo: local\n hooks:\n - id: a\n name: original\n", + id="duplicate-repository", + ), + ], +) +def test_human_cannot_relocate_managed_pin_through_duplicate_identity( + configs: tuple[Path, Path], check_config: Callable[..., None], proposal: str +) -> None: + base, proposed = configs + base.write_text( + "repos:\n - repo: local\n hooks:\n" + " - id: a\n name: original\n additional_dependencies:\n" + ' - "other==1.2.0" # release-creator managed\n' + ) + proposed.write_text(proposal) + + with pytest.raises(ValueError, match="managed hook dependencies need unique repository and hook IDs"): + check_config(base, proposed, False, 7, human=True) + + +def test_human_bootstrap_rejects_ambiguous_hook_identity( + configs: tuple[Path, Path], check_config: Callable[..., None] +) -> None: + base, proposed = configs + base.write_text("repos:\n - repo: local\n hooks:\n - id: a\n") + proposed.write_text( + "repos:\n - repo: local\n hooks:\n" + ' - id: a\n additional_dependencies:\n - "other==1.2.0" # release-creator managed\n' + " - id: a\n" + ) + + with pytest.raises(ValueError, match="managed hook dependencies need unique repository and hook IDs"): + check_config(base, proposed, False, 7, human=True) + + def test_human_can_bootstrap_managed_pins_from_release_migration() -> None: root = Path(__file__).resolve().parents[2] result = subprocess.run( diff --git a/tools/check_hook_config.py b/tools/check_hook_config.py index e6a5771..608cdc2 100644 --- a/tools/check_hook_config.py +++ b/tools/check_hook_config.py @@ -61,10 +61,9 @@ def check_config(base: Path, proposed: Path, managed: bool, minimum_days: int, * if human: if not isinstance(old.get("repos"), list) or not isinstance(new.get("repos"), list): raise TypeError("hook repositories must be lists") - old_managed = Counter(item for group in _dependency_groups(base_source) for item, managed in group if managed) - if old_managed and old_managed != Counter( - item for group in _dependency_groups(proposed_source) for item, managed in group if managed - ): + old_managed = _managed_pin_locations(base_source, old) + new_managed = _managed_pin_locations(proposed_source, new) + if old_managed and old_managed != new_managed: raise ValueError("managed hook dependencies require the release App") return old_content = deepcopy(old) @@ -95,6 +94,24 @@ def check_config(base: Path, proposed: Path, managed: bool, minimum_days: int, * ) +def _managed_pin_locations(source: str, config: dict[str, object]) -> Counter[tuple[str, str, str]]: + groups = iter(_dependency_groups(source)) + locations: Counter[tuple[str, str, str]] = Counter() + identities: set[tuple[str, str]] = set() + duplicate = False + for repository in cast("list[dict[str, object]]", config["repos"]): + url = cast("str", repository["repo"]) + for hook in cast("list[dict[str, object]]", repository["hooks"]): + hook_id = cast("str", hook["id"]) + if (url, hook_id) in identities: + duplicate = True + identities.add((url, hook_id)) + locations.update((url, hook_id, item) for item, managed in next(groups) if managed) + if locations and duplicate: + raise ValueError("managed hook dependencies need unique repository and hook IDs") + return locations + + def _dependency_groups(source: str) -> Iterator[list[tuple[str, bool]]]: root = cast("MappingNode", compose(source)) repositories = cast("SequenceNode", _value(root, "repos"))