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 11c7a4c..2f9eac9 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,32 @@ 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. 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: @@ -204,9 +186,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: @@ -667,25 +649,77 @@ 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 + deployments: 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 }} +``` + +`channel` is empty for Stable; use a Marketplace channel name for a prerelease. Keep the publishing step named +`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 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 +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. + +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 ``` -`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. +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 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..f221bb8 --- /dev/null +++ b/changelog.d/+hook-closure.bugfix.md @@ -0,0 +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. 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/changelog.d/+marketplace-retry.breaking.md b/changelog.d/+marketplace-retry.breaking.md new file mode 100644 index 0000000..7787b6e --- /dev/null +++ b/changelog.d/+marketplace-retry.breaking.md @@ -0,0 +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. 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 d836708..da545ea 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 and Deployments read permissions + required: true + publish-token: + description: JetBrains Marketplace upload token + required: true runs: using: composite steps: @@ -32,13 +53,188 @@ 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 + 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 + [[ "$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: 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 + 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") + 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 + 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 +242,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/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/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_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..d4310e5 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 @@ -33,12 +35,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 +106,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 +153,169 @@ 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) + + +@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( + [ + 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: @@ -146,18 +332,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 dbf1c1d..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)) @@ -428,6 +429,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/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 e7a443a..52a6dd3 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,648 @@ 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/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, + 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 + + +@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(), + 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", + 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, + 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}} + ) + 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"}]}]} + ) + artifacts = ( + [ + { + "id": artifact_id, + "name": artifact_name, + "expired": False, + "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"), + (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_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=str(current_attempt), + **environment, + **settings, + ) + + @pytest.fixture def signed_archive(tmp_path: Path) -> Path: archive = tmp_path / "Fish Shell 2-0.1.11-signed.zip" @@ -143,7 +814,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") 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..6d04c66 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,93 @@ 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, + capsys: pytest.CaptureFixture[str], + 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", + "-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" + 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() + assert "changes beyond expired package cutoffs" in capsys.readouterr().err + + +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", + "-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" + 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 +631,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 +650,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 +719,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..608cdc2 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") + 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) new_content = deepcopy(new) old_repos = old_content.get("repos") @@ -84,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")) @@ -152,22 +180,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