diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a2e64179..2b39b197 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,13 @@ updates: directory: / schedule: interval: daily + - package-ecosystem: uv + directory: / + schedule: + interval: daily + open-pull-requests-limit: 10 + + - package-ecosystem: pre-commit + directory: / + schedule: + interval: daily diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 00000000..3e5ceda6 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,37 @@ +--- +name: autofix.ci + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + autofix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v9.0.0 + + - name: Run fixers + uses: j178/prek-action@v3.0.0 + with: + prek-version: 0.4.11 + extra-args: >- + --all-files --hook-stage pre-commit --no-fail-fast --verbose + env: + UV_NO_CACHE: '1' + UV_PYTHON: 3.14 + + - uses: autofix-ci/action@v1.3.4 + if: always() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1449314e..663a6dc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,4 @@ --- - name: Test on: @@ -12,20 +11,23 @@ on: # Run at 1:00 every day - cron: 0 1 * * * +permissions: {} + jobs: build: - strategy: matrix: - python-version: ['3.13'] + python-version: ['3.14'] platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v9.0.0 with: enable-cache: true cache-dependency-glob: '**/pyproject.toml' @@ -34,16 +36,10 @@ jobs: run: | # Using -s is useful but also it is required to avoid # https://github.com/pallets/click/issues/824. - uv --verbose run --extra=dev pytest -s -vvv --cov-fail-under 100 --cov=src/ --cov=tests . --cov-report=xml + uv --verbose run --extra=dev pytest -s -vvv --cov-fail-under 100 --cov=src/ --cov=tests . env: UV_PYTHON: ${{ matrix.python-version }} - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} - completion-ci: needs: build runs-on: ubuntu-latest diff --git a/.github/workflows/dependabot-merge.yml b/.github/workflows/dependabot-merge.yml index 5238c9f6..69fa5303 100644 --- a/.github/workflows/dependabot-merge.yml +++ b/.github/workflows/dependabot-merge.yml @@ -10,13 +10,8 @@ permissions: jobs: dependabot: runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' + if: github.event.pull_request.user.login == 'dependabot[bot]' steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v2 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - name: Enable auto-merge for Dependabot PRs run: gh pr merge --auto --merge "$PR_URL" env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2ee9f7f9..c825288b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,5 +1,4 @@ --- - name: Lint on: @@ -12,40 +11,46 @@ on: # Run at 1:00 every day - cron: 0 1 * * * +permissions: {} + jobs: build: - strategy: matrix: - python-version: ['3.13'] + python-version: ['3.14'] platform: [ubuntu-latest, windows-latest] + hook-stage: [pre-commit, pre-push, manual] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v5 - # We need our tags in order to calculate the version + - uses: actions/checkout@v7 # We need our tags in order to calculate the version # in the Sphinx setup. + with: + persist-credentials: false fetch-depth: 0 fetch-tags: true - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v9.0.0 with: enable-cache: true cache-dependency-glob: '**/pyproject.toml' - name: Lint - run: | - uv run --extra=dev pre-commit run --all-files --hook-stage pre-commit --verbose - uv run --extra=dev pre-commit run --all-files --hook-stage pre-push --verbose - uv run --extra=dev pre-commit run --all-files --hook-stage manual --verbose + uses: j178/prek-action@v3.0.0 + with: + prek-version: 0.4.11 + extra-args: >- + --all-files --hook-stage ${{ matrix.hook-stage }} --verbose env: + UV_NO_CACHE: '1' UV_PYTHON: ${{ matrix.python-version }} - - - uses: pre-commit-ci/lite-action@v1.1.0 - if: always() + # hadolint is not available on Windows. + # nixfmt depends on the Haskell `unix` package, which is + # not buildable on Windows. + SKIP: ${{ runner.os == 'Windows' && 'hadolint,nixfmt' || '' }} completion-lint: needs: build diff --git a/.github/workflows/publish-site.yml b/.github/workflows/publish-site.yml index 2b2e80bb..3541c9d3 100644 --- a/.github/workflows/publish-site.yml +++ b/.github/workflows/publish-site.yml @@ -17,10 +17,11 @@ jobs: pages: write id-token: write steps: - - uses: actions/checkout@v5 - # We need our tags in order to calculate the version + - uses: actions/checkout@v7 # We need our tags in order to calculate the version # in the Sphinx setup. + with: + persist-credentials: false fetch-depth: 0 fetch-tags: true @@ -29,7 +30,7 @@ jobs: with: documentation_path: docs/source pyproject_extras: dev - python_version: '3.13' + python_version: '3.14' sphinx_build_options: -W publish: ${{ github.ref_name == 'main' }} checkout: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89bc0622..02d0e005 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,17 @@ --- - name: Release on: workflow_dispatch +permissions: + contents: read + jobs: build: name: Publish a release runs-on: ubuntu-latest + outputs: + new_tag: ${{ steps.tag_version.outputs.new_tag }} # Specifying an environment is strongly recommended by PyPI. # See https://github.com/pypa/gh-action-pypi-publish/tree/release/v1/?tab=readme-ov-file#trusted-publishing. @@ -21,7 +25,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: # See # https://github.com/stefanzweifel/git-auto-commit-action?tab=readme-ov-file#push-to-protected-branches @@ -34,7 +38,7 @@ jobs: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v9.0.0 with: enable-cache: true cache-dependency-glob: '**/pyproject.toml' @@ -48,26 +52,33 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Get the changelog underline - id: changelog_underline - run: | - underline="$(echo "${{ steps.calver.outputs.release }}" | tr -c '\n' '-')" - echo "underline=${underline}" >> "$GITHUB_OUTPUT" + # towncrier writes the rendered notes to stdout (informational + # chatter goes to stderr), so this is the curated release body for + # this version, not github-tag-action's commit-derived changelog. + - name: Generate the GitHub release notes + env: + RELEASE: ${{ steps.calver.outputs.release }} + run: uv run --extra=release towncrier build --draft --version "$RELEASE" > + release-notes.md - - name: Update changelog - uses: jacobtomlinson/gha-find-replace@v3 - with: - find: "Next\n----" - replace: "Next\n----\n\n${{ steps.calver.outputs.release }}\n${{ steps.changelog_underline.outputs.underline\ - \ }}" - include: CHANGELOG.rst - regex: false + # Assemble the same fragments into CHANGELOG.rst under a new + # ``$RELEASE`` section and delete the consumed fragment files. + - name: Update the changelog + env: + RELEASE: ${{ steps.calver.outputs.release }} + run: uv run --extra=release towncrier build --yes --version "$RELEASE" - - uses: stefanzweifel/git-auto-commit-action@v6 + - name: Update VERSION file for Nix flake + env: + RELEASE: ${{ steps.calver.outputs.release }} + run: | + echo "$RELEASE" > VERSION + + - uses: stefanzweifel/git-auto-commit-action@v7 id: commit with: - commit_message: Bump CHANGELOG - file_pattern: CHANGELOG.rst + commit_message: Bump CHANGELOG and VERSION + file_pattern: CHANGELOG.rst newsfragments VERSION # Error if there are no changes. skip_dirty_check: true @@ -81,16 +92,20 @@ jobs: commit_sha: ${{ steps.commit.outputs.commit_hash }} - name: Checkout the latest tag - the one we just created + env: + NEW_TAG: ${{ steps.tag_version.outputs.new_tag }} run: | git fetch --tags - git checkout ${{ steps.tag_version.outputs.new_tag }} + git checkout "$NEW_TAG" - name: Build a binary wheel and a source tarball id: build-wheel + env: + NEW_TAG: ${{ steps.tag_version.outputs.new_tag }} run: | sudo rm -rf dist/ build/ git fetch --tags - git checkout ${{ steps.tag_version.outputs.new_tag }} + git checkout "$NEW_TAG" uv build --sdist --wheel --out-dir dist/ WHEEL="$(ls dist/*.whl)" uv run --extra=release check-wheel-contents "${WHEEL}" @@ -108,11 +123,13 @@ jobs: # the pushed version. # Here, we give PyPI time to propagate the package. - name: Install package from PyPI - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 + env: + RELEASE: ${{ steps.calver.outputs.release }} with: timeout_seconds: 5 max_attempts: 50 - command: uv pip install --refresh vws-cli==${{ steps.calver.outputs.release }} + command: uv pip install --refresh vws-cli=="$RELEASE" - name: Set up Homebrew filename id: set-homebrew-filename @@ -122,14 +139,18 @@ jobs: # We still hit the race condition, so we have a retry here too. - name: Create a Homebrew recipe id: homebrew-create - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 + env: + RELEASE: ${{ steps.calver.outputs.release }} + HOMEBREW_FILENAME: ${{ steps.set-homebrew-filename.outputs.filename }} with: timeout_seconds: 5 max_attempts: 50 command: | - uv run --no-cache --with="vws-cli==${{ steps.calver.outputs.release }}" --extra=release poet --formula vws-cli > ${{ steps.set-homebrew-filename.outputs.filename }} + uv run --no-cache --with="vws-cli==$RELEASE" --extra=release poet --formula vws-cli > "$HOMEBREW_FILENAME" - name: Update Homebrew description + id: update_homebrew_description uses: jacobtomlinson/gha-find-replace@v3 with: find: desc "Shiny new formula" @@ -137,14 +158,14 @@ jobs: include: ${{ steps.set-homebrew-filename.outputs.filename }} regex: false - - name: Fix Homebrew class name - uses: jacobtomlinson/gha-find-replace@v3 - with: - find: class VWSCLI - replace: class VwsCli - include: ${{ steps.set-homebrew-filename.outputs.filename }} - regex: false - + - name: Check Update Homebrew description was modified + env: + MODIFIED_FILES: ${{ steps.update_homebrew_description.outputs.modifiedFiles }} + run: | + if [ "$MODIFIED_FILES" = "0" ]; then + echo "Error: No files were modified when updating Homebrew description" + exit 1 + fi - name: Push Homebrew Recipe uses: dmnemec/copy_file_to_another_repo_action@main env: @@ -161,38 +182,313 @@ jobs: user_name: adamtheturtle commit_message: Bump VWS CLI Homebrew recipe - - name: Create Linux binary for Vuforia Cloud Reco + - name: Create a GitHub release + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + makeLatest: true + name: Release ${{ steps.tag_version.outputs.new_tag }} + bodyFile: release-notes.md + + build-linux: + name: Build Linux binary (${{ matrix.binary.name }}) + needs: build + runs-on: ubuntu-latest + + strategy: + matrix: + binary: + - name: vuforia-cloud-reco-linux + spec: bin/vuforia-cloud-reco.py + - name: vws-linux + spec: bin/vuforia-web-services.py + - name: vumark-linux + spec: bin/vumark.py + + permissions: + contents: write + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: ${{ needs.build.outputs.new_tag }} + + # We have a race condition. + # In particular, we push to PyPI and then immediately try to install + # the pushed version. + # Here, we give PyPI time to propagate the package. + # We normalize the version (e.g., 2026.01.22 -> 2026.1.22) for PyPI lookup. + - name: Wait for PyPI propagation + uses: nick-fields/retry@v4 + env: + NEW_TAG: ${{ needs.build.outputs.new_tag }} + with: + timeout_seconds: 10 + max_attempts: 50 + command: | + normalized_version=$(echo "$NEW_TAG" | sed -E 's/\.0+([0-9])/.\1/g') + curl -sf "https://pypi.org/pypi/vws-cli/${normalized_version}/json" > /dev/null + + - name: Create requirements file + env: + NEW_TAG: ${{ needs.build.outputs.new_tag }} + run: echo "vws-cli==$NEW_TAG" > requirements.txt + + - name: Create Linux binary uses: sayyid5416/pyinstaller@v1 with: - python_ver: '3.13' + python_ver: '3.14' + pyinstaller_ver: ==6.12.0 + spec: ${{ matrix.binary.spec }} + requirements: requirements.txt + options: --onefile, --name "${{ matrix.binary.name }}" + upload_exe_with_name: ${{ matrix.binary.name }} + clean_checkout: false + + - name: Upload Linux binary to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NEW_TAG: ${{ needs.build.outputs.new_tag }} + run: | + gh release upload "$NEW_TAG" dist/${{ matrix.binary.name }} --clobber + + publish-docker: + name: Publish Docker image + needs: build + runs-on: ubuntu-latest + + permissions: + contents: read + # This is needed for pushing Docker images to ghcr.io. + packages: write + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: ${{ needs.build.outputs.new_tag }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # We have a race condition. + # In particular, we push to PyPI and then the Docker build + # fetches packages from PyPI inside the container. + # The Docker build may hit a different CDN node than the previous + # check, so we retry. + - name: Build and push Docker image + uses: nick-fields/retry@v4 + env: + NEW_TAG: ${{ needs.build.outputs.new_tag }} + with: + timeout_minutes: 10 + max_attempts: 5 + command: |- + docker buildx build \ + --push \ + --platform linux/amd64,linux/arm64 \ + --build-arg VWS_CLI_VERSION="$NEW_TAG" \ + --tag ghcr.io/vws-python/vws-cli:"$NEW_TAG" \ + --tag ghcr.io/vws-python/vws-cli:latest \ + . + + build-windows: + name: Build Windows binaries + needs: build + runs-on: windows-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: ${{ needs.build.outputs.new_tag }} + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: '3.14' + + # We have a race condition. + # In particular, we push to PyPI and then immediately try to install + # the pushed version. + # Here, we give PyPI time to propagate the package. + # We normalize the version (e.g., 2026.01.22 -> 2026.1.22) for PyPI lookup. + - name: Wait for PyPI propagation + uses: nick-fields/retry@v4 + env: + NEW_TAG: ${{ needs.build.outputs.new_tag }} + with: + timeout_seconds: 10 + max_attempts: 50 + shell: bash + command: | + normalized_version=$(echo "$NEW_TAG" | sed -E 's/\.0+([0-9])/.\1/g') + curl -sf "https://pypi.org/pypi/vws-cli/${normalized_version}/json" > /dev/null + + - name: Create requirements file + env: + NEW_TAG: ${{ needs.build.outputs.new_tag }} + run: echo "vws-cli==$NEW_TAG" > requirements.txt + + - name: Create Windows binary for Vuforia Cloud Reco + uses: sayyid5416/pyinstaller@v1 + with: + python_ver: '3.14' pyinstaller_ver: ==6.12.0 spec: bin/vuforia-cloud-reco.py - requirements: '`echo ${{ steps.build-wheel.outputs.wheel_filename }} > requirements.txt - && echo requirements.txt`' - options: --onefile, --name "vuforia-cloud-reco-linux" - upload_exe_with_name: vuforia-cloud-reco-linux + requirements: requirements.txt + options: --onefile, --name "vuforia-cloud-reco-windows" + upload_exe_with_name: vuforia-cloud-reco-windows clean_checkout: false - - name: Create Linux binary for Vuforia Web Services + - name: Create Windows binary for Vuforia Web Services uses: sayyid5416/pyinstaller@v1 with: - python_ver: '3.13' + python_ver: '3.14' pyinstaller_ver: ==6.12.0 spec: bin/vuforia-web-services.py - requirements: '`echo ${{ steps.build-wheel.outputs.wheel_filename }} > requirements.txt - && echo requirements.txt`' - options: --onefile, --name "vws-linux" - upload_exe_with_name: vws-linux + requirements: requirements.txt + options: --onefile, --name "vws-windows" + upload_exe_with_name: vws-windows clean_checkout: false - - name: Create a GitHub release - uses: ncipollo/release-action@v1 + - name: Create Windows binary for VuMark generation + uses: sayyid5416/pyinstaller@v1 with: - # Use specific artifact names (not a glob) so that we get a clear - # error if the artifact does not exist for some reason. - artifacts: dist/vws-linux,dist/vuforia-cloud-reco-linux - artifactErrorsFailBuild: true - tag: ${{ steps.tag_version.outputs.new_tag }} - makeLatest: true - name: Release ${{ steps.tag_version.outputs.new_tag }} - body: ${{ steps.tag_version.outputs.changelog }} + python_ver: '3.14' + pyinstaller_ver: ==6.12.0 + spec: bin/vumark.py + requirements: requirements.txt + options: --onefile, --name "vumark-windows" + upload_exe_with_name: vumark-windows + clean_checkout: false + + - name: Upload Windows binaries to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NEW_TAG: ${{ needs.build.outputs.new_tag }} + run: |- + gh release upload "$NEW_TAG" dist/vws-windows.exe --clobber + gh release upload "$NEW_TAG" dist/vuforia-cloud-reco-windows.exe --clobber + gh release upload "$NEW_TAG" dist/vumark-windows.exe --clobber + + build-macos: + name: Build macOS binaries + needs: build + runs-on: macos-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: ${{ needs.build.outputs.new_tag }} + + # We have a race condition. + # In particular, we push to PyPI and then immediately try to install + # the pushed version. + # Here, we give PyPI time to propagate the package. + # We normalize the version (e.g., 2026.01.22 -> 2026.1.22) for PyPI lookup. + - name: Wait for PyPI propagation + uses: nick-fields/retry@v4 + env: + NEW_TAG: ${{ needs.build.outputs.new_tag }} + with: + timeout_seconds: 10 + max_attempts: 50 + command: | + normalized_version=$(echo "$NEW_TAG" | sed -E 's/\.0+([0-9])/.\1/g') + curl -sf "https://pypi.org/pypi/vws-cli/${normalized_version}/json" > /dev/null + + - name: Create requirements file + env: + NEW_TAG: ${{ needs.build.outputs.new_tag }} + run: echo "vws-cli==$NEW_TAG" > requirements.txt + + - name: Create macOS binary for Vuforia Cloud Reco + uses: sayyid5416/pyinstaller@v1 + with: + python_ver: '3.14' + pyinstaller_ver: ==6.12.0 + spec: bin/vuforia-cloud-reco.py + requirements: requirements.txt + options: --onefile, --name "vuforia-cloud-reco-macos" + upload_exe_with_name: vuforia-cloud-reco-macos + clean_checkout: false + + - name: Create macOS binary for Vuforia Web Services + uses: sayyid5416/pyinstaller@v1 + with: + python_ver: '3.14' + pyinstaller_ver: ==6.12.0 + spec: bin/vuforia-web-services.py + requirements: requirements.txt + options: --onefile, --name "vws-macos" + upload_exe_with_name: vws-macos + clean_checkout: false + + - name: Create macOS binary for VuMark generation + uses: sayyid5416/pyinstaller@v1 + with: + python_ver: '3.14' + pyinstaller_ver: ==6.12.0 + spec: bin/vumark.py + requirements: requirements.txt + options: --onefile, --name "vumark-macos" + upload_exe_with_name: vumark-macos + clean_checkout: false + + - name: Upload macOS binaries to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NEW_TAG: ${{ needs.build.outputs.new_tag }} + run: |- + gh release upload "$NEW_TAG" dist/vws-macos --clobber + gh release upload "$NEW_TAG" dist/vuforia-cloud-reco-macos --clobber + gh release upload "$NEW_TAG" dist/vumark-macos --clobber + + publish-to-winget: + name: Publish to WinGet + needs: [build, build-windows] + runs-on: windows-latest + environment: winget + permissions: + contents: read + + steps: + - uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: VWSPython.vws-cli + version: ${{ needs.build.outputs.new_tag }} + release-tag: ${{ needs.build.outputs.new_tag }} + installers-regex: ^vws-windows\.exe$ + token: ${{ secrets.WINGET_TOKEN }} + + - uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: VWSPython.vuforia-cloud-reco + version: ${{ needs.build.outputs.new_tag }} + release-tag: ${{ needs.build.outputs.new_tag }} + installers-regex: ^vuforia-cloud-reco-windows\.exe$ + token: ${{ secrets.WINGET_TOKEN }} + + - uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: VWSPython.vumark + version: ${{ needs.build.outputs.new_tag }} + release-tag: ${{ needs.build.outputs.new_tag }} + installers-regex: ^vumark-windows\.exe$ + token: ${{ secrets.WINGET_TOKEN }} diff --git a/.gitignore b/.gitignore index 34c8a1e8..f698e90d 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,8 @@ src/*/_setuptools_scm_version.py .DS_Store **/.DS_Store -uv.lock +# Nix +result + +# Vale styles downloaded by ``vale sync`` +styles/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e7aa2be..7c2f3423 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,46 +1,18 @@ --- fail_fast: true -# We use system Python, with required dependencies specified in pyproject.toml. -# We therefore cannot use those dependencies in pre-commit CI. -ci: - skip: - - actionlint - - check-manifest - - deptry - - doc8 - - docformatter - - docs - - interrogate - - interrogate-docs - - linkcheck - - mypy - - mypy-docs - - pylint - - pyproject-fmt-fix - - pyright - - pyright-docs - - pyright-verifytypes - - pyroma - - ruff-check-fix - - ruff-check-fix-docs - - ruff-format-fix - - ruff-format-fix-docs - - shellcheck - - shellcheck-docs - - shfmt - - shfmt-docs - - spelling - - sphinx-lint - - vulture - - vulture-docs - - yamlfix +.uv_version: &uv_version uv==0.11.7 # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -default_install_hook_types: [pre-commit, pre-push, commit-msg] +default_install_hook_types: [pre-commit, pre-push] repos: + - repo: https://github.com/NixOS/nixfmt + rev: v1.4.0 + hooks: + - id: nixfmt + stages: [manual] - repo: meta hooks: - id: check-useless-excludes @@ -89,23 +61,42 @@ repos: stages: [pre-commit] - repo: local hooks: + - id: uv-lock + name: uv lock + entry: uv lock --locked + language: python + pass_filenames: false + files: (^pyproject\.toml$|^uv\.lock$) + additional_dependencies: + - *uv_version + stages: [pre-commit] + - id: actionlint name: actionlint entry: uv run --extra=dev actionlint language: python pass_filenames: false types_or: [yaml] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - - id: docformatter - name: docformatter - entry: uv run --extra=dev -m docformatter --in-place + - id: hadolint + name: hadolint + entry: uv run --extra=dev hadolint + language: python + files: Dockerfile + additional_dependencies: + - *uv_version + stages: [pre-commit] + + - id: pydocstringformatter + name: pydocstringformatter + entry: uv run --extra=dev pydocstringformatter language: python types_or: [python] - # We exclude this file to avoid https://github.com/PyCQA/docformatter/issues/291. - exclude: src/vws_cli/commands.py - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: shellcheck @@ -113,7 +104,8 @@ repos: entry: uv run --extra=dev shellcheck --shell=bash language: python types_or: [shell] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: shellcheck-docs @@ -122,16 +114,18 @@ repos: --command="shellcheck --shell=bash" language: python types_or: [markdown, rst] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: shfmt name: shfmt - entry: shfmt --write --space-redirects --indent=4 + entry: uv run --extra=dev shfmt --write --space-redirects --indent=4 language: python pass_filenames: false types_or: [shell] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: shfmt-docs @@ -140,24 +134,29 @@ repos: --no-pad-file --command="shfmt --write --space-redirects --indent=4" language: python types_or: [markdown, rst] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: mypy name: mypy stages: [pre-push] - entry: uv run --extra=dev -m mypy + entry: uv run --extra=dev -m mypy --num-workers=4 language: python types_or: [python, toml] pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version - id: mypy-docs name: mypy-docs stages: [pre-push] - entry: uv run --extra=dev doccmd --language=python --command="mypy" + entry: uv run --extra=dev doccmd --no-write-to-file --language=python --command="mypy + --num-workers=4" language: python types_or: [markdown, rst] + additional_dependencies: + - *uv_version - id: check-manifest name: check-manifest @@ -165,7 +164,8 @@ repos: entry: uv run --extra=dev -m check_manifest language: python pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version - id: pyright name: pyright @@ -174,12 +174,14 @@ repos: language: python types_or: [python, toml] pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version - id: pyright-docs name: pyright-docs stages: [pre-push] - entry: uv run --extra=dev doccmd --language=python --command="pyright" + entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python + --command="pyright" language: python types_or: [markdown, rst] @@ -189,16 +191,19 @@ repos: language: python types_or: [python] pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: vulture-docs name: vulture docs - entry: uv run --extra=dev doccmd --language=python --command="vulture" + entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python + --command="vulture" language: python types_or: [python] pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: pyroma @@ -207,7 +212,8 @@ repos: language: python pass_filenames: false types_or: [toml] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: deptry @@ -215,7 +221,8 @@ repos: entry: uv run --extra=dev -m deptry src/ language: python pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: pylint @@ -224,11 +231,13 @@ repos: language: python stages: [manual] pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version - id: pylint-docs name: pylint-docs - entry: uv run --extra=dev doccmd --language=python --command="pylint" + entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python + --command="pylint" language: python stages: [manual] types_or: [markdown, rst] @@ -238,7 +247,8 @@ repos: entry: uv run --extra=dev -m ruff check --fix language: python types_or: [python] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: ruff-check-fix-docs @@ -246,7 +256,8 @@ repos: entry: uv run --extra=dev doccmd --language=python --command="ruff check --fix" language: python types_or: [markdown, rst] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: ruff-format-fix @@ -254,7 +265,8 @@ repos: entry: uv run --extra=dev -m ruff format language: python types_or: [python] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: ruff-format-fix-docs @@ -263,15 +275,68 @@ repos: format" language: python types_or: [markdown, rst] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] + - id: strict-kwargs-fix + name: strict-kwargs + entry: uv run --extra=dev strict-kwargs check --fix + language: python + types_or: [python] + additional_dependencies: + - *uv_version + stages: [pre-commit] + require_serial: true + + - id: no-defaults + name: no-defaults + entry: uv run --extra=dev no-defaults + language: python + types_or: [python] + additional_dependencies: + - *uv_version + stages: [pre-commit] + require_serial: true + - id: interrogate-docs name: interrogate docs - entry: uv run --extra=dev doccmd --language=python --command="interrogate" + entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python + --command="interrogate" language: python types_or: [markdown, rst] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version + stages: [pre-commit] + + # Vale enforces prose style rules, such as banning em dashes, in + # reStructuredText and Markdown files. + # The rules come from the ``ai-tells`` package pinned in ``.vale.ini``, + # which ``vale sync`` downloads into the (gitignored) ``styles`` + # directory. + # Vale needs ``rst2html`` from Docutils on the ``PATH`` to parse + # reStructuredText. + # ``vale sync`` also runs when ``.vale.ini`` changes, so a package + # bump takes effect without waiting for the next reStructuredText + # change. + - id: vale-sync + name: vale sync + entry: uv run --extra=dev vale sync + language: python + pass_filenames: false + files: (\.(rst|md)$|^\.vale\.ini$) + additional_dependencies: + - *uv_version + stages: [pre-commit] + + - id: vale + name: vale + entry: uv run --extra=dev vale + language: python + types_or: [rst, markdown] + require_serial: true + additional_dependencies: + - *uv_version stages: [pre-commit] - id: doc8 @@ -279,7 +344,8 @@ repos: entry: uv run --extra=dev -m doc8 language: python types_or: [rst] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: interrogate @@ -300,29 +366,34 @@ repos: stages: [pre-commit] - id: linkcheck name: linkcheck - entry: make -C docs/ linkcheck SPHINXOPTS=-W + entry: uv run --extra=dev sphinx-build -M linkcheck docs/source docs/build + -W language: python types_or: [rst] stages: [manual] pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version - id: spelling name: spelling - entry: make -C docs/ spelling SPHINXOPTS=-W + entry: uv run --extra=dev sphinx-build -M spelling docs/source docs/build + -W language: python types_or: [rst] stages: [manual] pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version - id: docs name: Build Documentation - entry: make docs + entry: uv run --extra=dev sphinx-build -M html docs/source docs/build language: python stages: [manual] pass_filenames: false - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version - id: pyright-verifytypes name: pyright-verifytypes @@ -332,12 +403,43 @@ repos: pass_filenames: false types_or: [python] + - id: ty + name: ty + stages: [pre-push] + entry: uv run --extra=dev ty check + language: python + types_or: [python, toml] + pass_filenames: false + additional_dependencies: + - *uv_version + + - id: ty-docs + name: ty-docs + stages: [pre-push] + entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python + --command="ty check" + language: python + types_or: [markdown, rst] + additional_dependencies: + - *uv_version + - id: yamlfix name: yamlfix entry: uv run --extra=dev yamlfix language: python types_or: [yaml] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version + stages: [pre-commit] + + - id: zizmor + name: zizmor + entry: uv run --extra=dev zizmor --strict-collection .github + language: python + pass_filenames: false + types_or: [yaml] + additional_dependencies: + - *uv_version stages: [pre-commit] - id: sphinx-lint @@ -345,5 +447,26 @@ repos: entry: uv run --extra=dev sphinx-lint --enable=all --disable=line-too-long language: python types_or: [rst] - additional_dependencies: [uv==0.6.3] + additional_dependencies: + - *uv_version stages: [pre-commit] + + - id: pyrefly + name: pyrefly + stages: [pre-push] + entry: uv run --extra=dev pyrefly check + language: python + types_or: [python, toml] + pass_filenames: false + additional_dependencies: + - *uv_version + + - id: pyrefly-docs + name: pyrefly-docs + stages: [pre-push] + entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python + --command="pyrefly check" + language: python + types_or: [markdown, rst] + additional_dependencies: + - *uv_version diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..4a36aae8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "overrides": [ + { + "files": ["*.yaml", "*.yml"], + "options": { + "singleQuote": true, + "printWidth": 100 + } + } + ] +} diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 00000000..729a5e88 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,13 @@ +StylesPath = styles +MinAlertLevel = error + +Packages = https://github.com/tbhb/vale-ai-tells/releases/download/v1.29.0/ai-tells.zip + +[*.{rst,md}] +BasedOnStyles = ai-tells + +# These rules misclassify established technical, example, or release-note prose +# in this repository. All other ai-tells rules remain enforced. +ai-tells.FigurativeLands = NO +ai-tells.FormalTransitions = NO +ai-tells.VerbTricolon = NO diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 08484c48..3c6a7d06 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,61 @@ Changelog ========= -Next ----- +.. towncrier release notes start + +2026.02.22 +---------- + + +2026.02.15.2 +------------ + + +2026.02.15.1 +------------ + + +2026.02.15 +---------- + + +2026.02.07.2 +------------ + + +2026.02.07.1 +------------ + + +2026.02.07 +---------- + + +* Add support for installing with winget on Windows. + +2026.01.25.1 +------------ + + +2026.01.25 +---------- + + +2026.01.22.3 +------------ + + +2026.01.22.2 +------------ + + +2026.01.22.1 +------------ + + +2026.01.22 +---------- + 2025.03.10 ---------- diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f8ddc445 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.14-slim + +LABEL org.opencontainers.image.source="https://github.com/VWS-Python/vws-cli" +LABEL org.opencontainers.image.description="CLI for Vuforia Web Services" +LABEL org.opencontainers.image.licenses="MIT" + +ARG VWS_CLI_VERSION + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Install the package from PyPI with pinned version +RUN uv pip install --system --no-cache "vws-cli==${VWS_CLI_VERSION}" + +# Set up entrypoint - users can override with vuforia-cloud-reco +ENTRYPOINT ["vws"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..c9f18d1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Adam Dangoor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index 216a2574..00000000 --- a/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -SHELL := /bin/bash -euxo pipefail - -# Treat Sphinx warnings as errors -SPHINXOPTS := -W - -.PHONY: docs -docs: - make -C docs clean html SPHINXOPTS=$(SPHINXOPTS) - -.PHONY: open-docs -open-docs: - python -c 'import os, webbrowser; webbrowser.open("file://" + os.path.abspath("docs/build/html/index.html"))' diff --git a/README.rst b/README.rst index b646e435..2e531d51 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -|Build Status| |codecov| |PyPI| +|Build Status| |PyPI| vws-cli ======= @@ -34,6 +34,59 @@ Requires `Homebrew`_. .. _Homebrew: https://docs.brew.sh/Installation +With Nix +^^^^^^^^ + +Requires `Nix`_. + +.. code-block:: console + + $ nix --extra-experimental-features 'nix-command flakes' run "github:VWS-Python/vws-cli" -- --help + +To avoid passing ``--extra-experimental-features`` every time, `enable flakes`_ permanently. + +.. _Nix: https://nixos.org/download/ +.. _enable flakes: https://wiki.nixos.org/wiki/Flakes#Enabling_flakes_permanently + +Or add to your flake inputs: + +.. code-block:: nix + + { + inputs.vws-cli.url = "github:VWS-Python/vws-cli"; + } + +With Docker +^^^^^^^^^^^ + +.. code-block:: console + + $ docker run --rm "ghcr.io/vws-python/vws-cli" --help + +To use ``vuforia-cloud-reco``: + +.. code-block:: console + + $ docker run --rm --entrypoint vuforia-cloud-reco "ghcr.io/vws-python/vws-cli" --help + +To use ``vumark``: + +.. code-block:: console + + $ docker run --rm --entrypoint vumark "ghcr.io/vws-python/vws-cli" --help + +With winget (Windows) +^^^^^^^^^^^^^^^^^^^^^ + +Requires `winget`_. + +.. code-block:: console + + $ winget install --id VWSPython.vws-cli --source winget --exact + $ winget install --id VWSPython.vuforia-cloud-reco --source winget --exact + +.. _winget: https://docs.microsoft.com/windows/package-manager/winget/ + Pre-built Linux binaries ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -41,6 +94,23 @@ See the `full documentation`_ for details on how to install pre-built Linux bina .. _full documentation: https://vws-python.github.io/vws-cli/install.html#pre-built-linux-x86-binaries +Pre-built Windows binaries +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Download the Windows executables from the `latest release`_ and place them in a directory on your ``PATH``. +The filenames are: + +* ``vws-windows.exe`` +* ``vuforia-cloud-reco-windows.exe`` +* ``vumark-windows.exe`` + +.. _latest release: https://github.com/VWS-Python/vws-cli/releases/latest + +Pre-built macOS (ARM) binaries +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +See the `full documentation`_ for details on how to install pre-built macOS binaries. + Usage example ------------- @@ -78,8 +148,6 @@ See the `full documentation `__ for infor .. |Build Status| image:: https://github.com/VWS-Python/vws-cli/actions/workflows/ci.yml/badge.svg?branch=main :target: https://github.com/VWS-Python/vws-cli/actions -.. |codecov| image:: https://codecov.io/gh/VWS-Python/vws-cli/branch/main/graph/badge.svg - :target: https://codecov.io/gh/VWS-Python/vws-cli .. |PyPI| image:: https://badge.fury.io/py/VWS-CLI.svg :target: https://badge.fury.io/py/VWS-CLI -.. |minimum-python-version| replace:: 3.13 +.. |minimum-python-version| replace:: 3.14 diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..bd12d807 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2026.02.22 diff --git a/bin/vuforia-cloud-reco.py b/bin/vuforia-cloud-reco.py index 55438101..22d5bce7 100755 --- a/bin/vuforia-cloud-reco.py +++ b/bin/vuforia-cloud-reco.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 -""" -Run VWS Cloud Reco CLI. -""" +"""Run VWS Cloud Reco CLI.""" from vws_cli.query import vuforia_cloud_reco diff --git a/bin/vuforia-web-services.py b/bin/vuforia-web-services.py index 90d42003..fc86cecc 100755 --- a/bin/vuforia-web-services.py +++ b/bin/vuforia-web-services.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 -""" -Run VWS CLI. -""" +"""Run VWS CLI.""" from vws_cli import vws_group diff --git a/bin/vumark.py b/bin/vumark.py new file mode 100755 index 00000000..9f1ee02d --- /dev/null +++ b/bin/vumark.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +"""Run VuMark generation CLI.""" + +from vws_cli.vumark import generate_vumark + +generate_vumark() diff --git a/codecov.yaml b/codecov.yaml deleted file mode 100644 index 5c35baac..00000000 --- a/codecov.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -coverage: - status: - patch: - default: - # Require 100% test coverage. - target: 100% diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 7aba47ed..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @uv run --extra=dev $(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @uv run --extra=dev $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/__init__.py b/docs/source/__init__.py index b63eed5f..535ceb2e 100644 --- a/docs/source/__init__.py +++ b/docs/source/__init__.py @@ -1,3 +1 @@ -""" -Documentation. -""" +"""Documentation.""" diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 567b5ffd..27cc816d 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -8,3 +8,6 @@ Commands .. click:: vws_cli.query:vuforia_cloud_reco :prog: vuforia-cloud-reco :show-nested: + +.. click:: vws_cli.vumark:generate_vumark + :prog: vumark diff --git a/docs/source/conf.py b/docs/source/conf.py index 84a07124..9b7fa2bb 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -""" -Configuration for Sphinx. -""" +"""Configuration for Sphinx.""" # pylint: disable=invalid-name @@ -24,11 +22,20 @@ extensions = [ "sphinx_copybutton", "sphinxcontrib.spelling", + "sphinxcontrib.towncrier.ext", "sphinx_click.ext", "sphinx_inline_tabs", "sphinx_substitution_extensions", ] +# Render the unreleased ``newsfragments/`` entries into +# ``docs/source/unreleased.rst`` so the Sphinx spelling, doc-build and +# link-checking gates cover the prose before it is assembled into +# CHANGELOG.rst at release time. +towncrier_draft_autoversion_mode = "draft" +towncrier_draft_include_empty = True +towncrier_draft_working_directory = f"{_pyproject_file.parent}" + templates_path = ["_templates"] source_suffix = ".rst" master_doc = "index" @@ -99,6 +106,9 @@ html_show_sourcelink = False html_theme_options = { "sidebar_hide_name": False, + "source_repository": "https://github.com/VWS-Python/vws-cli/", + "source_branch": "main", + "source_directory": "docs/source/", } # Retry link checking to avoid transient network errors. @@ -119,4 +129,5 @@ .. |minimum-python-version| replace:: {minimum_python_version} .. |github-owner| replace:: VWS-Python .. |github-repository| replace:: vws-cli +.. |docker-image| replace:: ghcr.io/vws-python/vws-cli """ diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 21e5dbe8..ee4b19a7 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -31,7 +31,7 @@ Install ``pre-commit`` hooks: .. code-block:: console - $ pre-commit install + $ prek install Linting ------- @@ -40,9 +40,9 @@ Run lint tools either by committing, or with: .. code-block:: console - $ pre-commit run --all-files --hook-stage pre-commit --verbose - $ pre-commit run --all-files --hook-stage pre-push --verbose - $ pre-commit run --all-files --hook-stage manual --verbose + $ prek run --all-files --hook-stage pre-commit --verbose + $ prek run --all-files --hook-stage pre-push --verbose + $ prek run --all-files --hook-stage manual --verbose .. _Homebrew: https://brew.sh @@ -64,8 +64,8 @@ Run the following commands to build and view documentation locally: .. code-block:: console - $ make docs - $ make open-docs + $ uv run --extra=dev sphinx-build -M html docs/source docs/build -W + $ python -c 'import os, webbrowser; webbrowser.open("file://" + os.path.abspath("docs/build/html/index.html"))' Continuous integration ---------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 25754db6..78fe3f2c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,4 +20,5 @@ Reference commands contributing release-process + unreleased changelog diff --git a/docs/source/install.rst b/docs/source/install.rst index 1273db81..44de4ec1 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -22,6 +22,64 @@ Requires `Homebrew`_. .. _Homebrew: https://docs.brew.sh/Installation +With Nix +~~~~~~~~ + +Requires Nix_. + +.. code-block:: console + :substitutions: + + $ nix --extra-experimental-features 'nix-command flakes' develop "github:|github-owner|/|github-repository|" + +To avoid passing ``--extra-experimental-features`` every time, `enable flakes`_ permanently. + +.. _Nix: https://nixos.org/download/ +.. _enable flakes: https://wiki.nixos.org/wiki/Flakes#Enabling_flakes_permanently + +Or add to your flake inputs: + +.. code-block:: nix + :substitutions: + + { + inputs.vws-cli.url = "github:|github-owner|/|github-repository|"; + } + +With Docker +~~~~~~~~~~~ + +.. code-block:: console + :substitutions: + + $ docker run --rm "|docker-image|" --help + +To use ``vuforia-cloud-reco``: + +.. code-block:: console + :substitutions: + + $ docker run --rm --entrypoint vuforia-cloud-reco "|docker-image|" --help + +To use ``vumark``: + +.. code-block:: console + :substitutions: + + $ docker run --rm --entrypoint vumark "|docker-image|" --help + +With winget (Windows) +~~~~~~~~~~~~~~~~~~~~~ + +Requires winget_. + +.. code-block:: console + + $ winget install --id VWSPython.vws-cli --source winget --exact + $ winget install --id VWSPython.vuforia-cloud-reco --source winget --exact + +.. _winget: https://docs.microsoft.com/windows/package-manager/winget/ + Pre-built Linux (x86) binaries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -32,6 +90,41 @@ Pre-built Linux (x86) binaries chmod +x /usr/local/bin/vws $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vuforia-cloud-reco-linux" -o /usr/local/bin/vuforia-cloud-reco && chmod +x /usr/local/bin/vuforia-cloud-reco + $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vumark-linux" -o /usr/local/bin/vumark && + chmod +x /usr/local/bin/vumark + +Pre-built Windows binaries +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Download the Windows executables from the `latest release`_ and place them in a directory on your ``PATH``. +The filenames are: + +* ``vws-windows.exe`` +* ``vuforia-cloud-reco-windows.exe`` +* ``vumark-windows.exe`` + +.. _latest release: https://github.com/VWS-Python/vws-cli/releases/latest + +Pre-built macOS (ARM) binaries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: console + :substitutions: + + $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vws-macos" -o /usr/local/bin/vws && + chmod +x /usr/local/bin/vws + $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vuforia-cloud-reco-macos" -o /usr/local/bin/vuforia-cloud-reco && + chmod +x /usr/local/bin/vuforia-cloud-reco + $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vumark-macos" -o /usr/local/bin/vumark && + chmod +x /usr/local/bin/vumark + +You may need to remove the quarantine attribute to allow the binaries to run: + +.. code-block:: console + + $ xattr -d com.apple.quarantine /usr/local/bin/vws + $ xattr -d com.apple.quarantine /usr/local/bin/vuforia-cloud-reco + $ xattr -d com.apple.quarantine /usr/local/bin/vumark Shell completion ~~~~~~~~~~~~~~~~ diff --git a/docs/source/release-process.rst b/docs/source/release-process.rst index bfeb5899..22ce9df6 100644 --- a/docs/source/release-process.rst +++ b/docs/source/release-process.rst @@ -7,6 +7,10 @@ Outcomes * A new ``git`` tag available to install. * A new package on PyPI. * A new Homebrew recipe available to install. +* A new Docker image on GitHub Container Registry. +* New binary assets attached to the GitHub release. +* New Winget packages available to install for ``vws``, + ``vuforia-cloud-reco``, and ``vumark``. Perform a Release ~~~~~~~~~~~~~~~~~ diff --git a/docs/source/unreleased.rst b/docs/source/unreleased.rst new file mode 100644 index 00000000..22ac7472 --- /dev/null +++ b/docs/source/unreleased.rst @@ -0,0 +1,8 @@ +Unreleased changes +================== + +Changes that have landed on the main branch but are not yet part of a +tagged release. These entries are assembled into the +:doc:`changelog` when the next release is published. + +.. towncrier-draft-entries:: diff --git a/docs/source/usage-example.rst b/docs/source/usage-example.rst index 0f8faaa5..2f3dd790 100644 --- a/docs/source/usage-example.rst +++ b/docs/source/usage-example.rst @@ -1,7 +1,7 @@ Usage example ------------- -.. skip doccmd[shellcheck]: next +.. skip doccmd[shellcheck]: start .. code-block:: console @@ -22,3 +22,74 @@ Usage example - target_id: b60f60121d37418eb1de123c381b2af9 - target_id: e3a6e1a216ad4df3aaae1f6dd309c800 $ + +.. skip doccmd[shellcheck]: end + +Model Target datasets +~~~~~~~~~~~~~~~~~~~~~ + +Model Target dataset commands use OAuth2 credentials for the `Model Target Web API`_, rather than server keys. +Vuforia generates a dataset in the background, so create the dataset, wait for it, and then download it. + +.. _Model Target Web API: https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + +.. skip doccmd[shellcheck]: start + +.. code-block:: console + + $ vws create-model-target-dataset \ + --client-id "$MODEL_TARGET_CLIENT_ID" \ + --client-secret "$MODEL_TARGET_CLIENT_SECRET" \ + --name my_dataset_name \ + --target-sdk 10.29 \ + --model-name my_model_name \ + --cad-data-file ~/Documents/my_model.obj \ + --cad-data-format obj + 1f0f2b7c1f0f4b0a9a1c4b6b0a5b0f2b + $ vws wait-for-model-target-dataset-generated \ + --client-id "$MODEL_TARGET_CLIENT_ID" \ + --client-secret "$MODEL_TARGET_CLIENT_SECRET" \ + --dataset-uuid 1f0f2b7c1f0f4b0a9a1c4b6b0a5b0f2b + completed_at: '2026-08-14 12:00:00+00:00' + created_at: '2026-08-14 11:59:00+00:00' + dataset_uuid: 1f0f2b7c1f0f4b0a9a1c4b6b0a5b0f2b + error: null + eta: null + status: done + warning: null + $ vws download-model-target-dataset \ + --client-id "$MODEL_TARGET_CLIENT_ID" \ + --client-secret "$MODEL_TARGET_CLIENT_SECRET" \ + --dataset-uuid 1f0f2b7c1f0f4b0a9a1c4b6b0a5b0f2b \ + --output ~/Documents/my_dataset.zip + $ vws delete-model-target-dataset \ + --client-id "$MODEL_TARGET_CLIENT_ID" \ + --client-secret "$MODEL_TARGET_CLIENT_SECRET" \ + --dataset-uuid 1f0f2b7c1f0f4b0a9a1c4b6b0a5b0f2b + $ + +.. skip doccmd[shellcheck]: end + +Use ``--dataset-type advanced`` with each of these commands for an advanced dataset. +To give more than one model, or to give guide views, describe the models in a JSON file and give it with ``--models-file``: + +.. code-block:: json + + { + "models": [ + { + "name": "my_model_name", + "cadDataUrl": "https://example.com/my_model.zip", + "cadDataFormat": "OBJ", + "views": [ + { + "name": "front", + "guideViewPosition": { + "rotation": [0, 0, 0, 1], + "translation": [0, 0, -1.5] + } + } + ] + } + ] + } diff --git a/docs/towncrier_template.rst.jinja b/docs/towncrier_template.rst.jinja new file mode 100644 index 00000000..6da87833 --- /dev/null +++ b/docs/towncrier_template.rst.jinja @@ -0,0 +1,14 @@ + +{% for section_name, section in sections.items() %} +{% if section %} +{% for category, entries in section.items() %} +{% for text, _ in entries.items() %} +- {{ text }} + +{% endfor %} +{% endfor %} +{% else %} +No significant changes. + +{% endif %} +{% endfor %} diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..48ff2c41 --- /dev/null +++ b/flake.lock @@ -0,0 +1,99 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1768886240, + "narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ], + "uv2nix": [ + "uv2nix" + ] + }, + "locked": { + "lastModified": 1763662255, + "narHash": "sha256-4bocaOyLa3AfiS8KrWjZQYu+IAta05u3gYZzZ6zXbT0=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "042904167604c681a090c07eb6967b4dd4dae88c", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768984832, + "narHash": "sha256-Q/5ICDALWlG+MbfLoghvXb3K9S0jqGt0mj8Y8RN+60k=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "c372512cad744dbdeb65dba864376a0a5ab339a7", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "uv2nix": "uv2nix" + } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1768526251, + "narHash": "sha256-uT0ICn7tknFtdOP7zhkgOkusq38EcttqUCS5Ho7nIvc=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "1b8f4d4c874f909de24639142c1141c84119b0f0", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..fbc6c377 --- /dev/null +++ b/flake.nix @@ -0,0 +1,148 @@ +{ + description = "A CLI for the Vuforia Web Services (VWS) API"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + pyproject-nix = { + url = "github:pyproject-nix/pyproject.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + uv2nix = { + url = "github:pyproject-nix/uv2nix"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + pyproject-build-systems = { + url = "github:pyproject-nix/build-system-pkgs"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.uv2nix.follows = "uv2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + pyproject-nix, + uv2nix, + pyproject-build-systems, + ... + }: + let + inherit (nixpkgs) lib; + forAllSystems = lib.genAttrs lib.systems.flakeExposed; + + workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; }; + + overlay = workspace.mkPyprojectOverlay { + sourcePreference = "wheel"; + }; + + # Read version from VERSION file, fall back to commit hash for dev builds + version = + let + versionFile = ./VERSION; + in + if builtins.pathExists versionFile then + lib.strings.trim (builtins.readFile versionFile) + else + "0.0.0+${self.shortRev or self.dirtyShortRev or "unknown"}"; + + # Override vws-cli to set the version + vwsCliOverlay = final: prev: { + vws-cli = prev.vws-cli.overrideAttrs (old: { + env = (old.env or { }) // { + SETUPTOOLS_SCM_PRETEND_VERSION = version; + }; + }); + }; + + pythonSets = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + python = pkgs.python314; + in + (pkgs.callPackage pyproject-nix.build.packages { + inherit python; + }).overrideScope + ( + lib.composeManyExtensions [ + pyproject-build-systems.overlays.wheel + overlay + vwsCliOverlay + ] + ) + ); + + in + { + packages = forAllSystems ( + system: + let + pythonSet = pythonSets.${system}; + virtualenv = pythonSet.mkVirtualEnv "vws-cli-env" workspace.deps.default; + in + { + default = virtualenv; + vws-cli = virtualenv; + } + ); + + apps = forAllSystems (system: { + default = { + type = "app"; + program = "${self.packages.${system}.default}/bin/vws"; + }; + vws = { + type = "app"; + program = "${self.packages.${system}.default}/bin/vws"; + }; + vuforia-cloud-reco = { + type = "app"; + program = "${self.packages.${system}.default}/bin/vuforia-cloud-reco"; + }; + }); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + pythonSet = pythonSets.${system}; + virtualenv = pythonSet.mkVirtualEnv "vws-cli-dev-env" workspace.deps.all; + in + { + default = pkgs.mkShell { + packages = [ + virtualenv + pkgs.uv + ]; + env = { + UV_NO_SYNC = "1"; + UV_PYTHON = pythonSet.python.interpreter; + UV_PYTHON_DOWNLOADS = "never"; + }; + shellHook = '' + unset PYTHONPATH + ''; + }; + } + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + vws-cli-smoke = pkgs.runCommand "vws-cli-smoke-test" { } '' + ${self.packages.${system}.default}/bin/vws --help > $out + ''; + } + ); + }; +} diff --git a/newsfragments/.gitkeep b/newsfragments/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/newsfragments/2434.change.rst b/newsfragments/2434.change.rst new file mode 100644 index 00000000..428a0e16 --- /dev/null +++ b/newsfragments/2434.change.rst @@ -0,0 +1 @@ +Drop Python 3.13 support, and update VWS Python so that the documented ``ProjectHasNoApiAccess`` result code spelling gives a friendly error message. diff --git a/newsfragments/2440.change.rst b/newsfragments/2440.change.rst new file mode 100644 index 00000000..3813ac27 --- /dev/null +++ b/newsfragments/2440.change.rst @@ -0,0 +1 @@ +Add Model Target dataset commands: ``vws create-model-target-dataset``, ``vws get-model-target-dataset-status``, ``vws wait-for-model-target-dataset-generated``, ``vws download-model-target-dataset`` and ``vws delete-model-target-dataset``. diff --git a/newsfragments/2441.change.rst b/newsfragments/2441.change.rst new file mode 100644 index 00000000..11c566cb --- /dev/null +++ b/newsfragments/2441.change.rst @@ -0,0 +1 @@ +Add a ``vws get-database-reco-counts-report`` command, which requests a per-target recognition counts report for a month, waits for Vuforia to generate it, and writes the CSV to stdout or to a given path. diff --git a/pyproject.toml b/pyproject.toml index dcd7515f..577a36d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,104 +18,128 @@ license = "MIT" authors = [ { name = "Adam Dangoor", email = "adamdangoor@gmail.com" }, ] -requires-python = ">=3.13" +requires-python = ">=3.14" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dynamic = [ "version", ] dependencies = [ - "beartype==0.21.0", - "click==8.2.1", - "pyyaml==6.0.2", - "vws-python==2025.3.10.1", + "beartype==0.22.9", + "click==8.4.2", + "pyyaml==6.0.3", + "vws-python==2026.8.14", ] optional-dependencies.dev = [ - "actionlint-py==1.7.7.23", - "check-manifest==0.50", - "deptry==0.23.1", + "actionlint-py==1.7.12.24", + "check-manifest==0.51", + "deptry==0.25.1", "doc8==2.0.0", - "doccmd==2025.4.8", - "docformatter==1.7.7", + "doccmd==2026.7.19", "freezegun==1.5.5", - "furo==2025.7.19", + "furo==2025.12.19", + "hadolint-bin==2.15.1; sys_platform!='win32'", "interrogate==1.7.0", - "mypy[faster-cache]==1.18.1", - "mypy-strict-kwargs==2025.4.3", - "pre-commit==4.3.0", - "pylint==3.3.8", - "pyproject-fmt==2.6.0", - "pyright==1.1.405", - "pyroma==5.0", - "pytest==8.4.2", - "pytest-cov==7.0.0", - "pytest-regressions==2.8.3", - "ruff==0.13.0", - "setuptools-scm==9.2.0", + "mypy[faster-cache]==2.3.0", + "mypy-strict-kwargs==2026.7.19.1", + "no-defaults==2.1.0", + "prek==0.4.13", + "pydocstringformatter==1.0.0", + "pylint[spelling]==4.0.7", + "pyproject-fmt==2.27.0", + "pyrefly==1.2.0", + "pyright==1.1.411", + "pyroma==5.0.1", + "pytest==9.1.1", + "pytest-beartype-tests==2026.4.26", + "pytest-cov==7.1.0", + "pytest-regressions==2.11.0", + "ruff==0.16.2", + "setuptools-scm==10.2.1", # We add shellcheck-py not only for shell scripts and shell code blocks, # but also because having it installed means that ``actionlint-py`` will # use it to lint shell commands in GitHub workflow files. "shellcheck-py==0.11.0.1", - "shfmt-py==3.12.0.2", - "sphinx==8.2.3", - "sphinx-click==6.1.0", + "shfmt-py==4.0.0", + "sphinx==9.1.0", + "sphinx-click==6.2.0", "sphinx-copybutton==0.5.2", - "sphinx-inline-tabs==2023.4.21", - "sphinx-lint==1.0.0", + "sphinx-inline-tabs==2025.12.21.14", + "sphinx-lint==1.0.2", "sphinx-pyproject==0.3.0", - "sphinx-substitution-extensions==2025.6.6", - "sphinxcontrib-spelling==8.0.1", - "types-pyyaml==6.0.12.20250822", - "vulture==2.14", - "vws-python-mock==2025.3.10.1", + "sphinx-substitution-extensions==2026.8.5", + "sphinxcontrib-spelling==8.0.2", + # ``sphinxcontrib-towncrier`` renders unreleased news fragments + # into docs/source/unreleased.rst during Sphinx builds. + "sphinxcontrib-towncrier==0.5.0a0", + # Listed explicitly (despite being transitive via vws-python-mock) so that + # [tool.uv.sources] can redirect to the CPU-only PyTorch index. + # See: https://vws-python.github.io/vws-python-mock/installation.html#faster-installation + "strict-kwargs==2026.7.24", + "torch>=2.5.1", + "torchvision>=0.20.1", + "towncrier==25.8.0", + "ty==0.0.70", + "types-pyyaml==6.0.12.20260724", + "vale==3.13.0.0", + "vulture==2.16", + "vws-python-mock==2026.8.14", "vws-test-fixtures==2023.3.5", - "yamlfix==1.18.0", + "yamlfix==1.19.1", + "zizmor==1.29.0", ] optional-dependencies.release = [ "check-wheel-contents==0.6.3", "homebrew-pypi-poet==0.10.0", + "towncrier==25.8.0", ] urls.Documentation = "https://vws-python.github.io/vws-cli/" urls.Source = "https://github.com/VWS-Python/vws-cli" scripts.vuforia-cloud-reco = "vws_cli.query:vuforia_cloud_reco" +scripts.vumark = "vws_cli.vumark:generate_vumark" scripts.vws = "vws_cli:vws_group" -[tool.setuptools] -zip-safe = false +[dependency-groups] +dev = [] -[tool.setuptools.packages.find] -where = [ +[tool.setuptools] +packages.find.where = [ "src", ] - -[tool.setuptools.package-data] -vws_cli = [ +package-data.vws_cli = [ "py.typed", ] +zip-safe = false [tool.setuptools_scm] # We write the version to a file so that we can import it. # We choose a ``.py`` file so that we can read it without # worrying about including the file in MANIFEST.in. -write_to = "src/vws_cli/_setuptools_scm_version.py" +version_file = "src/vws_cli/_setuptools_scm_version.py" + +[tool.uv] +sources.torch = { index = "pytorch-cpu" } +sources.torchvision = { index = "pytorch-cpu" } +index = [ { name = "pytorch-cpu", url = "https://download.pytorch.org/whl/cpu", explicit = true } ] [tool.ruff] line-length = 79 - lint.select = [ "ALL", ] lint.ignore = [ # Ruff warns that this conflicts with the formatter. "COM812", - # Allow our chosen docstring line-style - no one-line summary. - "D200", + # Copyright headers are not required. + "CPY001", + # Allow our chosen docstring line-style - pydocstringformatter handles formatting + # but doesn't enforce D205 (blank line after summary) or D212 (summary on first line). "D205", "D212", # Allow backslashes in a docstring. @@ -125,85 +149,41 @@ lint.ignore = [ "E501", # Ruff warns that this conflicts with the formatter. "ISC001", - # Ignore "too-many-*" errors as they seem to get in the way more than + # Ignore 'too-many-*' errors as they seem to get in the way more than # helping. "PLR0913", # Allow 'assert' as we use it for tests and type narrowing. "S101", + # On Python 3.14, annotations are lazily evaluated (PEP 649), so Ruff + # suggests moving annotation-only imports into type-checking blocks. + # ``beartype`` resolves annotations at runtime, in source code and in + # tests, so those imports must be available at runtime. + "TC001", + "TC002", + "TC003", +] +lint.per-file-ignores."doccmd_*.py" = [ + # Allow our chosen docstring line-style - pydocstringformatter handles + # formatting but docstrings in docs may not match this style. + "D200", + # Allow asserts in docs. + "S101", ] - lint.per-file-ignores."tests/test_*.py" = [ # Do not require tests to have a one-line summary. "D205", ] - # Do not automatically remove commented out code. # We comment out code during development, and with VSCode auto-save, this code # is sometimes annoyingly removed. lint.unfixable = [ "ERA001", ] +lint.external = [ "NOD" ] +lint.flake8-tidy-imports.banned-api."typing.cast".msg = "typing.cast is banned: use explicit type narrowing or a typed variable instead." lint.pydocstyle.convention = "google" [tool.pylint] - -[tool.pylint.'MASTER'] - -# Pickle collected data for later comparisons. -persistent = true - -# Use multiple processes to speed up Pylint. -jobs = 0 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -# See https://chezsoi.org/lucas/blog/pylint-strict-base-configuration.html. -# We do not use the plugins: -# - pylint.extensions.code_style -# - pylint.extensions.magic_value -# - pylint.extensions.while_used -# as they seemed to get in the way. -load-plugins = [ - 'pylint.extensions.bad_builtin', - 'pylint.extensions.comparison_placement', - 'pylint.extensions.consider_refactoring_into_while_condition', - 'pylint.extensions.docparams', - 'pylint.extensions.dunder', - 'pylint.extensions.eq_without_hash', - 'pylint.extensions.for_any_all', - 'pylint.extensions.mccabe', - 'pylint.extensions.no_self_use', - 'pylint.extensions.overlapping_exceptions', - 'pylint.extensions.private_import', - 'pylint.extensions.redefined_loop_name', - 'pylint.extensions.redefined_variable_type', - 'pylint.extensions.set_membership', - 'pylint.extensions.typing', -] - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension = false - -ignore = [ - '_setuptools_scm_version.py', -] - -[tool.pylint.'MESSAGES CONTROL'] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable = [ - 'bad-inline-option', - 'deprecated-pragma', - 'file-ignored', - 'spelling', - 'use-symbolic-message-instead', - 'useless-suppression', -] - # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration @@ -213,162 +193,176 @@ enable = [ # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" - -disable = [ - 'too-few-public-methods', - 'too-many-locals', - 'too-many-arguments', - 'too-many-instance-attributes', - 'too-many-return-statements', - 'too-many-lines', - 'locally-disabled', +"MESSAGES CONTROL".disable = [ + # Too difficult to please + "duplicate-code", # Let ruff handle long lines - 'line-too-long', - # Let ruff handle unused imports - 'unused-import', - # Let ruff deal with sorting - 'ungrouped-imports', + "line-too-long", + "locally-disabled", + "missing-return-type-doc", # We don't need everything to be documented because of mypy - 'missing-type-doc', - 'missing-return-type-doc', - # Too difficult to please - 'duplicate-code', + "missing-type-doc", + "too-few-public-methods", + "too-many-arguments", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-return-statements", + # Let ruff deal with sorting + "ungrouped-imports", + # Let ruff handle unused imports + "unused-import", # Let ruff handle imports - 'wrong-import-order', + "wrong-import-order", +] +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +"MESSAGES CONTROL".enable = [ + "bad-inline-option", + "deprecated-pragma", + "file-ignored", + "spelling", + "use-symbolic-message-instead", + "useless-suppression", +] +DEPRECATED_BUILTINS.bad-functions = [ + # Use Pylint until Ruff can ban bare builtin calls, or until custom rules + # make this removable: + # https://github.com/astral-sh/ruff/issues/10079 + # https://github.com/astral-sh/ruff/issues/970 + "filter", + "getattr", + "hasattr", + "map", + "setattr", ] - -[tool.pylint.'FORMAT'] - # Allow the body of an if to be on the same line as the test if there is no # else. -single-line-if-stmt = false - -[tool.pylint.'SPELLING'] - +FORMAT.single-line-if-stmt = false +# Return non-zero exit code if useless-suppression is emitted. +MAIN.fail-on = [ + "useless-suppression", +] +MASTER.ignore = [ + "_setuptools_scm_version.py", +] +# Use multiple processes to speed up Pylint. +MASTER.jobs = 0 +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +# See https://chezsoi.org/lucas/blog/pylint-strict-base-configuration.html. +# We do not use the plugins: +# - pylint.extensions.code_style +# - pylint.extensions.magic_value +# - pylint.extensions.while_used +# as they seemed to get in the way. +MASTER.load-plugins = [ + "pylint.extensions.bad_builtin", + "pylint.extensions.comparison_placement", + "pylint.extensions.consider_refactoring_into_while_condition", + "pylint.extensions.docparams", + "pylint.extensions.dunder", + "pylint.extensions.eq_without_hash", + "pylint.extensions.for_any_all", + "pylint.extensions.mccabe", + "pylint.extensions.no_self_use", + "pylint.extensions.overlapping_exceptions", + "pylint.extensions.private_import", + "pylint.extensions.redefined_loop_name", + "pylint.extensions.redefined_variable_type", + "pylint.extensions.set_membership", + "pylint.extensions.typing", +] +# Pickle collected data for later comparisons. +MASTER.persistent = true +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +MASTER.unsafe-load-any-extension = false # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. -spelling-dict = 'en_US' - +SPELLING.spelling-dict = "en_US" # A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file = 'spelling_private_dict.txt' - +SPELLING.spelling-private-dict-file = "spelling_private_dict.txt" # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words = 'no' +SPELLING.spelling-store-unknown-words = "no" -[tool.docformatter] -make-summary-multi-line = true +[tool.interrogate] +fail-under = 100 +exclude = [ + "src/*/_setuptools_scm_version.py", +] +verbose = 2 +omit-covered-files = true [tool.check-manifest] - ignore = [ - ".checkmake-config.ini", - ".yamlfmt", "*.enc", + ".checkmake-config.ini", + ".git_archival.txt", ".pre-commit-config.yaml", + ".prettierrc", + ".vale.ini", + ".yamlfmt", + "bin", + "bin/*", "CHANGELOG.rst", - "CODE_OF_CONDUCT.rst", - "CONTRIBUTING.rst", - "LICENSE", - "Makefile", "ci", "ci/**", - "codecov.yaml", + "CODE_OF_CONDUCT.rst", + "CONTRIBUTING.rst", "docs", "docs/**", - ".git_archival.txt", + "flake.lock", + "flake.nix", + "LICENSE", + "lint.mk", + "Makefile", + "newsfragments", + "newsfragments/**", "spelling_private_dict.txt", + "src/*/_setuptools_scm_version.py", "tests", "tests-pylintrc", "tests/**", "vuforia_secrets.env.example", - "lint.mk", - "bin", - "bin/*", - "src/*/_setuptools_scm_version.py", + "zizmor.yml", ] [tool.deptry] -pep621_dev_dependency_groups = [ +# torch and torchvision are listed explicitly in dev deps to allow +# [tool.uv.sources] to redirect them to the CPU-only PyTorch index, +# but they are not directly imported in the vws-cli source code. +per_rule_ignores.DEP002 = [ + "torch", + "torchvision", +] +optional_dependencies_dev_groups = [ "dev", "packaging", "release", ] -[tool.pyproject-fmt] -indent = 4 -keep_full_version = true -max_supported_python = "3.13" - -[tool.pytest.ini_options] - -xfail_strict = true -log_cli = true - -[tool.coverage.run] - -branch = true -omit = [ - 'src/vws_cli/_setuptools_scm_version.py', -] - -[tool.coverage.report] -exclude_also = [ - "if TYPE_CHECKING:", -] - -[tool.mypy] - -strict = true -files = [ "." ] -exclude = [ "build" ] -follow_untyped_imports = true -plugins = [ - "mypy_strict_kwargs", -] - -[tool.pyright] - -enableTypeIgnoreComments = false -reportUnnecessaryTypeIgnoreComment = true -typeCheckingMode = "strict" - -[tool.interrogate] -fail-under = 100 -omit-covered-files = true -verbose = 2 +[tool.vulture] exclude = [ - "src/*/_setuptools_scm_version.py", -] - -[tool.doc8] - -max_line_length = 2000 -ignore_path = [ - "./.eggs", - "./docs/build", - "./docs/build/spelling/output.txt", - "./node_modules", - "./src/*.egg-info/", - "./src/*/_setuptools_scm_version.txt", + # Duplicate some of .gitignore + ".venv", + # We ignore the version file as it is generated by setuptools_scm. + "_setuptools_scm_version.py", ] - -[tool.vulture] # Ideally we would limit the paths to the source code where we want to ignore names, # but Vulture does not enable this. ignore_names = [ - # pytest configuration - "pytest_collect_file", - "pytest_collection_modifyitems", - "pytest_plugins", - # pytest fixtures - we name fixtures like this for this purpose - "fixture_*", # Sphinx "autoclass_content", "autoclass_content", "autodoc_member_order", "copybutton_exclude", "extensions", + # pytest fixtures - we name fixtures like this for this purpose + "fixture_*", "html_show_copyright", "html_show_sourcelink", "html_show_sphinx", @@ -384,20 +378,101 @@ ignore_names = [ "nitpicky", "project_copyright", "pygments_style", + # pytest configuration + "pytest_collect_file", + "pytest_plugins", "rst_prolog", "source_suffix", "spelling_word_list_filename", "templates_path", + "towncrier_draft_autoversion_mode", + "towncrier_draft_include_empty", + "towncrier_draft_working_directory", "warning_is_error", ] -exclude = [ - # Duplicate some of .gitignore - ".venv", - # We ignore the version file as it is generated by setuptools_scm. - "_setuptools_scm_version.py", +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true +max_supported_python = "3.14" + +[tool.mypy] +files = [ "." ] +exclude = [ "build" ] +exclude_gitignore = true +follow_untyped_imports = true +strict = true +plugins = [ + "mypy_strict_kwargs", +] + +[tool.pyrefly] +errors.non-exhaustive-match = "error" + +[tool.pyright] +typeCheckingMode = "strict" +enableTypeIgnoreComments = false +reportUnnecessaryTypeIgnoreComment = true + +[tool.pytest] +log_cli = true +xfail_strict = true + +[tool.coverage] +run.branch = true +run.omit = [ + "src/vws_cli/_setuptools_scm_version.py", +] +report.exclude_also = [ + "if TYPE_CHECKING:", +] +report.show_missing = true + +[tool.towncrier] +# The changelog and the per-release GitHub release notes are both built +# from news fragments under ``newsfragments/``. The release workflow +# runs ``towncrier build`` to assemble them; contributors add one +# fragment file per user-facing change. +directory = "newsfragments" +filename = "CHANGELOG.rst" +# Custom template so an assembled version reproduces the historical +# style exactly: a bare ```` heading (no project name, no +# date) followed by a flat bullet list with no per-type sub-headings. +template = "docs/towncrier_template.rst.jinja" +title_format = "{version}" +issue_format = "#{issue}" +# ``title_format`` underline first, then any nested headings. A bare +# version such as ``2026.05.18`` underlined with ``-`` matches every +# pre-towncrier entry in CHANGELOG.rst. +underlines = [ "-", "~", "^" ] +type = [ + # A single, unnamed fragment type keeps the assembled output as one + # flat bullet list, matching the historical changelog (which never + # grouped entries under "Features"/"Bugfixes"/... sub-headings). + { directory = "change", name = "", showcontent = true }, +] + +[tool.pydocstringformatter] +write = true +split-summary-body = false +max-line-length = 75 +linewrap-full-docstring = true + +[tool.doc8] +max_line_length = 2000 +ignore_path = [ + "./.eggs", + "./docs/build", + "./docs/build/spelling/output.txt", + "./node_modules", + "./src/*.egg-info/", + "./src/*/_setuptools_scm_version.txt", ] [tool.yamlfix] section_whitelines = 1 whitelines = 1 + +[tool.no_defaults] +private_only = true +per_file_enforcement."tests/**" = "all" diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index 066a3619..c45f684b 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -1,5 +1,9 @@ +CSV +OAuth +Winget admin api +ar args beartype changelog @@ -7,6 +11,8 @@ cli dockerfile dockerfiles dulwich +entrypoint +executables gzip homebrew linter @@ -17,6 +23,8 @@ macOS metadata noqa num +pdf +png pragma pre pyperclip @@ -28,9 +36,13 @@ reportMissingTypeStubs reportUnknownArgumentType reportUnknownMemberType reportUnknownVariableType +stdout +svg typeshed ubuntu versioned vuforia +vumark vwq vws +winget diff --git a/src/vws_cli/__init__.py b/src/vws_cli/__init__.py index b62566c0..5077ff4d 100644 --- a/src/vws_cli/__init__.py +++ b/src/vws_cli/__init__.py @@ -1,6 +1,4 @@ -""" -A CLI for Vuforia Web Services. -""" +"""A CLI for Vuforia Web Services.""" from importlib.metadata import PackageNotFoundError, version @@ -10,6 +8,7 @@ from vws_cli.commands import ( add_target, delete_target, + get_database_reco_counts_report, get_database_summary_report, get_duplicate_targets, get_target_record, @@ -18,6 +17,13 @@ update_target, wait_for_target_processed, ) +from vws_cli.model_target import ( + create_model_target_dataset, + delete_model_target_dataset, + download_model_target_dataset, + get_model_target_dataset_status, + wait_for_model_target_dataset_generated, +) _CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]} @@ -38,17 +44,21 @@ @click.version_option(version=__version__) @beartype def vws_group() -> None: - """ - Manage a Vuforia Web Services cloud database. - """ + """Manage a Vuforia Web Services cloud database.""" vws_group.add_command(cmd=add_target) +vws_group.add_command(cmd=create_model_target_dataset) +vws_group.add_command(cmd=delete_model_target_dataset) vws_group.add_command(cmd=delete_target) +vws_group.add_command(cmd=download_model_target_dataset) +vws_group.add_command(cmd=get_database_reco_counts_report) vws_group.add_command(cmd=get_database_summary_report) vws_group.add_command(cmd=get_duplicate_targets) +vws_group.add_command(cmd=get_model_target_dataset_status) vws_group.add_command(cmd=get_target_record) vws_group.add_command(cmd=get_target_summary_report) vws_group.add_command(cmd=list_targets) vws_group.add_command(cmd=update_target) +vws_group.add_command(cmd=wait_for_model_target_dataset_generated) vws_group.add_command(cmd=wait_for_target_processed) diff --git a/src/vws_cli/_error_handling.py b/src/vws_cli/_error_handling.py new file mode 100644 index 00000000..63272a2b --- /dev/null +++ b/src/vws_cli/_error_handling.py @@ -0,0 +1,116 @@ +"""Error handling utilities for the VWS CLI.""" + +from beartype import beartype +from vws.exceptions.custom_exceptions import ( + RecoCountsReportDownloadError, + RecoCountsReportTimeoutError, + ServerError, + TargetProcessingTimeoutError, +) +from vws.exceptions.model_target_exceptions import ( + ModelTargetAuthenticationError, + ModelTargetDatasetNotDoneError, + ModelTargetError, + ModelTargetOAuth2Error, + ModelTargetValidationError, + UnknownModelTargetDatasetError, +) +from vws.exceptions.vws_exceptions import ( + AuthenticationFailureError, + BadImageError, + DateRangeError, + FailError, + ImageTooLargeError, + MetadataTooLargeError, + ProjectHasNoAPIAccessError, + ProjectInactiveError, + ProjectSuspendedError, + RequestQuotaReachedError, + RequestTimeTooSkewedError, + TargetNameExistError, + TargetQuotaReachedError, + TargetStatusNotSuccessError, + TargetStatusProcessingError, + UnknownTargetError, +) + + +@beartype +def get_error_message(exc: Exception) -> str: + """Get an error message from a VWS exception.""" + exc_type_to_message: dict[type[Exception], str] = { + AuthenticationFailureError: "The given secret key was incorrect.", + BadImageError: "Error: The given image is corrupted or the format is not supported.", + DateRangeError: "Error: There was a problem with the date details given in the request.", + FailError: "Error: The request made to Vuforia was invalid and could not be processed. Check the given parameters.", + ImageTooLargeError: "Error: The given image is too large.", + MetadataTooLargeError: "Error: The given metadata is too large.", + RecoCountsReportDownloadError: "Error: The recognition counts report could not be downloaded. This may be because the report's URL has expired.", + RecoCountsReportTimeoutError: "Error: The recognition counts report was not generated within the allowed limit.", + ServerError: "Error: There was an unknown error from Vuforia. This may be because there is a problem with the given name.", + ProjectInactiveError: "Error: The project associated with the given keys is inactive.", + RequestQuotaReachedError: "Error: The maximum number of API calls for this database has been reached.", + RequestTimeTooSkewedError: "Error: Vuforia reported that the time given with this request was outside the expected range. This may be because the system clock is out of sync.", + TargetProcessingTimeoutError: "Error: The target processing time has exceeded the allowed limit.", + TargetQuotaReachedError: "Error: The maximum number of targets for this database has been reached.", + ProjectSuspendedError: "Error: The request could not be completed because this database has been suspended.", + ProjectHasNoAPIAccessError: "Error: The request could not be completed because this database is not allowed to make API requests.", + } + + match exc: + case UnknownTargetError(): + return f'Error: Target "{exc.target_id}" does not exist.' + case TargetNameExistError(): + return ( + f'Error: There is already a target named "{exc.target_name}".' + ) + case TargetStatusNotSuccessError(): + return ( + f'Error: The target "{exc.target_id}" cannot be updated as it is ' + "in the processing state." + ) + case TargetStatusProcessingError(): + return ( + f'Error: The target "{exc.target_id}" cannot be deleted as it is ' + "in the processing state." + ) + case _: + return exc_type_to_message[type(exc)] + + +@beartype +def get_model_target_error_message( + exc: ModelTargetError | ModelTargetOAuth2Error | ServerError, +) -> str: + """Get an error message from a Model Target Web API exception.""" + match exc: + case ModelTargetOAuth2Error(): + message = ( + "Error: The given client ID and client secret are not a set " + "of Model Target Web API credentials." + ) + case ModelTargetAuthenticationError(): + message = "Error: The request to Vuforia was not authenticated." + case UnknownModelTargetDatasetError(): + message = ( + "Error: No Model Target dataset of the given type matches " + "the given UUID." + ) + case ModelTargetDatasetNotDoneError(): + message = ( + "Error: Vuforia has not finished generating the dataset, so " + "the dataset cannot be downloaded." + ) + case ModelTargetValidationError(): + problems = [ + f"{detail.code}: {detail.message}" for detail in exc.details + ] or [exc.message] + message = "\n".join( + ["Error: Vuforia rejected the request.", *problems], + ) + case ModelTargetError(): + message = f"Error: {exc.message or 'Vuforia returned an error.'}" + case _: + message = get_error_message(exc=exc) + + return message diff --git a/src/vws_cli/commands.py b/src/vws_cli/commands.py index 931a4f09..42180d5a 100644 --- a/src/vws_cli/commands.py +++ b/src/vws_cli/commands.py @@ -1,11 +1,14 @@ """``click`` commands the VWS CLI.""" +import calendar import contextlib import dataclasses +import datetime import io import sys -from collections.abc import Callable, Iterator, Mapping +from collections.abc import Generator from pathlib import Path +from zoneinfo import ZoneInfo import click import yaml @@ -13,28 +16,14 @@ from vws import VWS from vws.exceptions.base_exceptions import VWSError from vws.exceptions.custom_exceptions import ( + RecoCountsReportDownloadError, + RecoCountsReportTimeoutError, ServerError, TargetProcessingTimeoutError, ) -from vws.exceptions.vws_exceptions import ( - AuthenticationFailureError, - BadImageError, - DateRangeError, - FailError, - ImageTooLargeError, - MetadataTooLargeError, - ProjectHasNoAPIAccessError, - ProjectInactiveError, - ProjectSuspendedError, - RequestQuotaReachedError, - RequestTimeTooSkewedError, - TargetNameExistError, - TargetQuotaReachedError, - TargetStatusNotSuccessError, - TargetStatusProcessingError, - UnknownTargetError, -) +from vws.exceptions.vws_exceptions import AuthenticationFailureError +from vws_cli._error_handling import get_error_message from vws_cli.options.credentials import ( server_access_key_option, server_secret_key_option, @@ -48,52 +37,16 @@ target_name_option, target_width_option, ) - - -@beartype -def _get_error_message(exc: Exception) -> str: - """Get an error message from a VWS exception.""" - if isinstance(exc, UnknownTargetError): - return f'Error: Target "{exc.target_id}" does not exist.' - - if isinstance(exc, TargetNameExistError): - return f'Error: There is already a target named "{exc.target_name}".' - - if isinstance(exc, TargetStatusNotSuccessError): - return ( - f'Error: The target "{exc.target_id}" cannot be updated as it is ' - "in the processing state." - ) - - if isinstance(exc, TargetStatusProcessingError): - return ( - f'Error: The target "{exc.target_id}" cannot be deleted as it is ' - "in the processing state." - ) - - exc_type_to_message: Mapping[type[Exception], str] = { - AuthenticationFailureError: "The given secret key was incorrect.", - BadImageError: "Error: The given image is corrupted or the format is not supported.", - DateRangeError: "Error: There was a problem with the date details given in the request.", - FailError: "Error: The request made to Vuforia was invalid and could not be processed. Check the given parameters.", - ImageTooLargeError: "Error: The given image is too large.", - MetadataTooLargeError: "Error: The given metadata is too large.", - ServerError: "Error: There was an unknown error from Vuforia. This may be because there is a problem with the given name.", - ProjectInactiveError: "Error: The project associated with the given keys is inactive.", - RequestQuotaReachedError: "Error: The maximum number of API calls for this database has been reached.", - RequestTimeTooSkewedError: "Error: Vuforia reported that the time given with this request was outside the expected range. This may be because the system clock is out of sync.", - TargetProcessingTimeoutError: "Error: The target processing time has exceeded the allowed limit.", - TargetQuotaReachedError: "Error: The maximum number of targets for this database has been reached.", - ProjectSuspendedError: "Error: The request could not be completed because this database has been suspended.", - ProjectHasNoAPIAccessError: "Error: The request could not be completed because this database is not allowed to make API requests.", - } - - return exc_type_to_message[type(exc)] +from vws_cli.options.timeout import ( + connection_timeout_seconds_option, + read_timeout_seconds_option, +) +from vws_cli.options.vws import base_vws_url_option, database_id_option @beartype @contextlib.contextmanager -def handle_vws_exceptions() -> Iterator[None]: +def _handle_vws_exceptions() -> Generator[None]: """Show error messages and catch exceptions from ``VWS-Python``.""" error_message = "" @@ -101,10 +54,11 @@ def handle_vws_exceptions() -> Iterator[None]: yield except ( VWSError, + RecoCountsReportDownloadError, ServerError, TargetProcessingTimeoutError, ) as exc: - error_message = _get_error_message(exc=exc) + error_message = get_error_message(exc=exc) else: return @@ -112,35 +66,23 @@ def handle_vws_exceptions() -> Iterator[None]: sys.exit(1) -def base_vws_url_option(command: Callable[..., None]) -> Callable[..., None]: - """An option decorator for choosing the base VWS URL.""" - click_option_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.option( - "--base-vws-url", - type=click.STRING, - default="https://vws.vuforia.com", - help="The base URL for the VWS API.", - show_default=True, - ) - - function: Callable[..., None] = click_option_function(command) - return function - - @click.command(name="get-target-record") @server_access_key_option @server_secret_key_option @target_id_option @base_vws_url_option -@handle_vws_exceptions() +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_vws_exceptions() @beartype def get_target_record( + *, server_access_key: str, server_secret_key: str, target_id: str, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, ) -> None: """Get a target record. @@ -152,6 +94,10 @@ def get_target_record( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) record = vws_client.get_target_record(target_id=target_id).target_record @@ -162,13 +108,18 @@ def get_target_record( @click.command(name="list-targets") @server_access_key_option @server_secret_key_option -@handle_vws_exceptions() +@_handle_vws_exceptions() @base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option @beartype def list_targets( + *, server_access_key: str, server_secret_key: str, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, ) -> None: """List targets. @@ -180,6 +131,10 @@ def list_targets( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) targets = vws_client.list_targets() yaml_list = yaml.dump(data=targets) @@ -190,14 +145,19 @@ def list_targets( @server_access_key_option @server_secret_key_option @target_id_option -@handle_vws_exceptions() +@_handle_vws_exceptions() @base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option @beartype def get_duplicate_targets( + *, server_access_key: str, server_secret_key: str, target_id: str, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, ) -> None: """Get a list of potential duplicate targets. @@ -209,6 +169,10 @@ def get_duplicate_targets( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) record = vws_client.get_duplicate_targets(target_id=target_id) @@ -219,13 +183,18 @@ def get_duplicate_targets( @click.command(name="get-database-summary-report") @server_access_key_option @server_secret_key_option -@handle_vws_exceptions() +@_handle_vws_exceptions() @base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option @beartype def get_database_summary_report( + *, server_access_key: str, server_secret_key: str, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, ) -> None: """Get a database summary report. @@ -237,6 +206,10 @@ def get_database_summary_report( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) report = vws_client.get_database_summary_report() yaml_report = yaml.dump(data=dataclasses.asdict(obj=report)) @@ -247,14 +220,19 @@ def get_database_summary_report( @server_access_key_option @server_secret_key_option @target_id_option -@handle_vws_exceptions() +@_handle_vws_exceptions() @base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option @beartype def get_target_summary_report( + *, server_access_key: str, server_secret_key: str, target_id: str, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, ) -> None: """Get a target summary report. @@ -266,6 +244,10 @@ def get_target_summary_report( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) report = vws_client.get_target_summary_report(target_id=target_id) report_dict = dataclasses.asdict(obj=report) @@ -279,14 +261,19 @@ def get_target_summary_report( @server_access_key_option @server_secret_key_option @target_id_option -@handle_vws_exceptions() +@_handle_vws_exceptions() @base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option @beartype def delete_target( + *, server_access_key: str, server_secret_key: str, target_id: str, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, ) -> None: """Delete a target. @@ -298,6 +285,10 @@ def delete_target( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) vws_client.delete_target(target_id=target_id) @@ -311,8 +302,10 @@ def delete_target( @target_name_option(required=True) @target_image_option(required=True) @active_flag_option(allow_none=False) -@handle_vws_exceptions() +@_handle_vws_exceptions() @base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option @beartype def add_target( *, @@ -323,6 +316,8 @@ def add_target( image_file_path: Path, active_flag_choice: ActiveFlagChoice, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, application_metadata: str | None = None, ) -> None: """Add a target. @@ -335,6 +330,10 @@ def add_target( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) image_bytes = image_file_path.read_bytes() @@ -365,8 +364,10 @@ def add_target( @target_image_option(required=False) @active_flag_option(allow_none=True) @target_id_option -@handle_vws_exceptions() +@_handle_vws_exceptions() @base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option @beartype def update_target( *, @@ -375,6 +376,8 @@ def update_target( target_id: str, image_file_path: Path | None, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, name: str | None = None, application_metadata: str | None = None, active_flag_choice: ActiveFlagChoice | None = None, @@ -390,6 +393,10 @@ def update_target( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) if image_file_path is None: @@ -448,7 +455,9 @@ def update_target( @server_secret_key_option @target_id_option @base_vws_url_option -@handle_vws_exceptions() +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_vws_exceptions() @beartype def wait_for_target_processed( *, @@ -457,13 +466,21 @@ def wait_for_target_processed( target_id: str, seconds_between_requests: float, base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, timeout_seconds: float, ) -> None: - """Wait for a target to be "processed". This is done by polling the VWS API.""" + """Wait for a target to be "processed". This is done by polling the VWS + API. + """ vws_client = VWS( server_access_key=server_access_key, server_secret_key=server_secret_key, base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) try: @@ -478,3 +495,183 @@ def wait_for_target_processed( err=True, ) sys.exit(1) + + +_MONTH_FORMAT = "%Y-%m" + +_REPORT_SECONDS_BETWEEN_REQUESTS_HELP = ( + "The number of seconds to wait between requests made while polling for " + "the report. " + f"We wait {_SECONDS_BETWEEN_REQUESTS_DEFAULT} seconds by default, rather " + "than less, than that to decrease the number of calls made to the API, to " + "decrease the likelihood of hitting the request quota." +) + +_REPORT_TIMEOUT_SECONDS_HELP = ( + "The maximum number of seconds to wait for the report to be generated." +) + + +@beartype +def _default_month() -> str: + """Return the current month, in the form which ``--month`` takes.""" + now = datetime.datetime.now(tz=ZoneInfo(key="UTC")) + return now.strftime(format=_MONTH_FORMAT) + + +@beartype +def _validate_month( + ctx: click.Context, + param: click.Parameter, + value: str, +) -> datetime.date: + """Turn a ``YYYY-mm`` string into the first day of that month.""" + # These are given by ``click``, and we do not use them. + del ctx + del param + try: + parsed = datetime.datetime.strptime( # noqa: DTZ007 + value, + _MONTH_FORMAT, + ) + except ValueError as exc: + message = f'"{value}" is not a month in the YYYY-mm form.' + raise click.BadParameter(message=message) from exc + return parsed.date() + + +@click.command(name="get-database-reco-counts-report") +@click.option( + "--month", + type=str, + default=_default_month, + callback=_validate_month, + help=( + "The month to get recognition counts for, in the YYYY-mm form. " + "Vuforia accepts only the current month and the previous month." + ), + show_default="the current month", +) +@click.option( + "--output", + "output_file_path", + type=click.Path( + dir_okay=False, + writable=True, + path_type=Path, + ), + required=False, + help=( + "The path to write the CSV report to. By default, the report is " + "written to stdout." + ), +) +@click.option( + "--no-wait", + "no_wait", + is_flag=True, + default=False, + help=( + "Do not wait for the report to be generated. Instead, show the URL " + "to download the report from once it has been generated." + ), +) +@click.option( + "--seconds-between-requests", + type=click.FloatRange(min=0.05), + default=_SECONDS_BETWEEN_REQUESTS_DEFAULT, + help=_REPORT_SECONDS_BETWEEN_REQUESTS_HELP, + show_default=True, +) +@click.option( + "--timeout-seconds", + type=click.FloatRange(min=0.05), + default=300, + help=_REPORT_TIMEOUT_SECONDS_HELP, + show_default=True, +) +@server_access_key_option +@server_secret_key_option +@database_id_option +@base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_vws_exceptions() +@beartype +def get_database_reco_counts_report( + *, + server_access_key: str, + server_secret_key: str, + database_id: str, + month: datetime.date, + output_file_path: Path | None, + no_wait: bool, + seconds_between_requests: float, + timeout_seconds: float, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> None: + """Get a per-target recognition counts report for a database. + + The report is a CSV with a ``target_id,reco_count`` header. Vuforia + generates the report in the background, so by default we wait for the + report to be generated before downloading it. + + \b + See + https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api. + """ + if no_wait and output_file_path is not None: + message = "--output cannot be used with --no-wait." + raise click.UsageError(message=message) + + vws_client = VWS( + server_access_key=server_access_key, + server_secret_key=server_secret_key, + base_vws_url=base_vws_url, + database_id=database_id, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), + ) + + try: + report_request = vws_client.request_database_reco_counts_report( + year=month.year, + month=calendar.Month(value=month.month), + ) + except AuthenticationFailureError: + click.echo( + message=( + "Error: The given secret key was incorrect, or the given " + "database ID is not the ID of the database which the given " + "server keys belong to." + ), + err=True, + ) + sys.exit(1) + + if no_wait: + click.echo(message=report_request.presigned_url) + return + + try: + report = vws_client.wait_for_reco_counts_report( + presigned_url=report_request.presigned_url, + seconds_between_requests=seconds_between_requests, + timeout_seconds=timeout_seconds, + ) + except RecoCountsReportTimeoutError: + click.echo( + message=f"Timeout of {timeout_seconds} seconds reached.", + err=True, + ) + sys.exit(1) + + if output_file_path is None: + click.echo(message=report.raw_csv, nl=False) + return + + output_file_path.write_bytes(data=report.raw_csv) diff --git a/src/vws_cli/model_target.py b/src/vws_cli/model_target.py new file mode 100644 index 00000000..39089f38 --- /dev/null +++ b/src/vws_cli/model_target.py @@ -0,0 +1,835 @@ +"""``click`` commands for the Vuforia Model Target Web API.""" + +import base64 +import contextlib +import dataclasses +import json +import sys +from collections.abc import Generator, Sequence +from enum import StrEnum +from pathlib import Path +from typing import Any + +import click +import yaml +from beartype import beartype +from vws import ModelTargetService +from vws.exceptions.custom_exceptions import ServerError +from vws.exceptions.model_target_exceptions import ( + ModelTargetDatasetTimeoutError, + ModelTargetError, + ModelTargetOAuth2Error, +) +from vws.model_target_datasets import ( + AutomaticColoring, + CadDataFormat, + GuideViewPosition, + ModelTargetDatasetType, + ModelTargetModel, + ModelTargetView, + MotionHint, + OptimizeTrackingFor, + RealisticAppearance, + Simplify, + TrackingMode, +) +from vws.reports import ( + ModelTargetDatasetStatuses, + ModelTargetDatasetStatusReport, +) + +from vws_cli._error_handling import get_model_target_error_message +from vws_cli.options.model_targets import ( + client_id_option, + client_secret_option, + dataset_type_option, + dataset_uuid_option, +) +from vws_cli.options.timeout import ( + connection_timeout_seconds_option, + read_timeout_seconds_option, +) +from vws_cli.options.vws import base_vws_url_option + +_SECONDS_BETWEEN_REQUESTS_DEFAULT = 0.2 + +_SECONDS_BETWEEN_REQUESTS_HELP = ( + "The number of seconds to wait between requests made while polling the " + "dataset status. " + f"We wait {_SECONDS_BETWEEN_REQUESTS_DEFAULT} seconds by default, rather " + "than less, than that to decrease the number of calls made to the API, to " + "decrease the likelihood of hitting the request quota." +) + +_TIMEOUT_SECONDS_HELP = ( + "The maximum number of seconds to wait for the dataset to be generated." +) + + +@beartype +@contextlib.contextmanager +def _handle_model_target_exceptions() -> Generator[None]: + """Show error messages and catch exceptions from ``VWS-Python``.""" + error_message = "" + + try: + yield + except (ModelTargetError, ModelTargetOAuth2Error, ServerError) as exc: + error_message = get_model_target_error_message(exc=exc) + else: + return + + click.echo(message=error_message, err=True) + sys.exit(1) + + +@beartype +def _model_target_client( + *, + client_id: str, + client_secret: str, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> ModelTargetService: + """Get a client for the Model Target Web API.""" + return ModelTargetService( + client_id=client_id, + client_secret=client_secret, + base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), + ) + + +@beartype +def _status_report_yaml(*, report: ModelTargetDatasetStatusReport) -> str: + """Get a YAML representation of a dataset status report.""" + report_dict = dataclasses.asdict(obj=report) + report_dict["status"] = report.status.value + for key in ("created_at", "eta", "completed_at"): + value = report_dict[key] + report_dict[key] = None if value is None else str(object=value) + return yaml.dump(data=report_dict) + + +_MODELS_FILE_HINT = "'--models-file'" + +_MODEL_STRING_FIELDS = { + "cadDataBlob": "cad_data_blob", + "cadDataUrl": "cad_data_url", + "name": "name", + "stateBasedConfigurationJsonString": ( + "state_based_configuration_json_string" + ), +} + +_MODEL_ENUM_FIELDS: dict[str, tuple[str, type[StrEnum]]] = { + "automaticColoring": ("automatic_coloring", AutomaticColoring), + "cadDataFormat": ("cad_data_format", CadDataFormat), + "motionHint": ("motion_hint", MotionHint), + "optimizeTrackingFor": ("optimize_tracking_for", OptimizeTrackingFor), + "realisticAppearance": ("realistic_appearance", RealisticAppearance), + "simplify": ("simplify", Simplify), + "trackingMode": ("tracking_mode", TrackingMode), +} + +_MODEL_FIELDS = frozenset( + {*_MODEL_STRING_FIELDS, *_MODEL_ENUM_FIELDS, "views"}, +) + +_VIEW_FIELDS = frozenset({"guideViewPosition", "name", "states"}) + +_POSITION_FIELDS = frozenset({"rotation", "translation"}) + + +@beartype +def _models_file_error(*, message: str) -> click.BadParameter: + """Get an error to raise for an invalid models file.""" + return click.BadParameter(message=message, param_hint=_MODELS_FILE_HINT) + + +@beartype +def _is_json_object(*, value: object) -> bool: + """Get whether a value from a models file is an object.""" + return isinstance(value, dict) + + +@beartype +def _is_json_array(*, value: object) -> bool: + """Get whether a value from a models file is an array.""" + return isinstance(value, list) + + +@beartype +def _as_json_object(*, value: object) -> dict[str, Any] | None: + """Get an object from a models file, or ``None``.""" + if not _is_json_object(value=value): + return None + # The value goes through a variable which is typed as ``Any`` so that + # the keys and values of the returned object are not unknown types. + value_any: Any = value + value_dict: dict[str, Any] = value_any + return value_dict + + +@beartype +def _json_object(*, value: object, message: str) -> dict[str, Any]: + """Get an object from a models file, or raise an error.""" + value_dict = _as_json_object(value=value) + if value_dict is None: + raise _models_file_error(message=message) + return value_dict + + +@beartype +def _json_array(*, value: object, message: str) -> list[Any]: + """Get an array from a models file, or raise an error.""" + if not _is_json_array(value=value): + raise _models_file_error(message=message) + # The value goes through a variable which is typed as ``Any`` so that + # the items of the returned array are not unknown types. + value_any: Any = value + value_list: list[Any] = value_any + return value_list + + +@beartype +def _checked_object( + *, + value: object, + known_fields: frozenset[str], + required_fields: Sequence[str], + path: str, +) -> dict[str, Any]: + """Get an object with known and required fields, or raise an error.""" + value_dict = _json_object( + value=value, + message=f"{path} must be an object.", + ) + unknown_fields = sorted(set(value_dict) - known_fields) + if unknown_fields: + message = f"{path} has unknown fields: {', '.join(unknown_fields)}." + raise _models_file_error(message=message) + + for required_field in required_fields: + if required_field not in value_dict: + message = f"{path}/{required_field} is required." + raise _models_file_error(message=message) + + return value_dict + + +@beartype +def _string_value(*, value: object, path: str) -> str: + """Get a string from a models file, or raise an error.""" + if not isinstance(value, str): + message = f"{path} must be a string." + raise _models_file_error(message=message) + return value + + +@beartype +def _enum_value( + *, + value: object, + enum_type: type[StrEnum], + path: str, +) -> StrEnum: + """Get an enumeration member from a models file, or raise an error.""" + string_value = _string_value(value=value, path=path) + try: + return enum_type(value=string_value) + except ValueError as exc: + allowed = ", ".join(sorted(member.value for member in enum_type)) + message = f"{path} must be one of: {allowed}." + raise _models_file_error(message=message) from exc + + +@beartype +def _number_sequence(*, value: object, path: str) -> Sequence[float]: + """Get a sequence of numbers from a models file, or raise an error.""" + message = f"{path} must be an array of numbers." + items = _json_array(value=value, message=message) + for item in items: + if isinstance(item, bool) or not isinstance(item, int | float): + raise _models_file_error(message=message) + + return [float(item) for item in items] + + +@beartype +def _guide_view_position_from_json( + *, + value: object, + path: str, +) -> GuideViewPosition: + """Get a guide view position from a models file, or raise an error.""" + position_dict = _checked_object( + value=value, + known_fields=_POSITION_FIELDS, + required_fields=("rotation", "translation"), + path=path, + ) + return GuideViewPosition( + rotation=_number_sequence( + value=position_dict["rotation"], + path=f"{path}/rotation", + ), + translation=_number_sequence( + value=position_dict["translation"], + path=f"{path}/translation", + ), + ) + + +@beartype +def _view_from_json(*, value: object, path: str) -> ModelTargetView: + """Get a guide view from a models file, or raise an error.""" + view_dict = _checked_object( + value=value, + known_fields=_VIEW_FIELDS, + required_fields=("guideViewPosition", "name"), + path=path, + ) + + states: Sequence[str] | None = None + if "states" in view_dict: + states_items = _json_array( + value=view_dict["states"], + message=f"{path}/states must be an array of strings.", + ) + states = [ + _string_value(value=state, path=f"{path}/states({index})") + for index, state in enumerate(iterable=states_items) + ] + + return ModelTargetView( + name=_string_value(value=view_dict["name"], path=f"{path}/name"), + guide_view_position=_guide_view_position_from_json( + value=view_dict["guideViewPosition"], + path=f"{path}/guideViewPosition", + ), + states=states, + ) + + +@beartype +def _model_from_json(*, value: object, path: str) -> ModelTargetModel: + """Get a model from a models file, or raise an error.""" + model_dict = _checked_object( + value=value, + known_fields=_MODEL_FIELDS, + required_fields=("name",), + path=path, + ) + + model_kwargs: dict[str, Any] = { + field_name: _string_value( + value=model_dict[json_field], + path=f"{path}/{json_field}", + ) + for json_field, field_name in _MODEL_STRING_FIELDS.items() + if json_field in model_dict + } + + for json_field, (field_name, enum_type) in _MODEL_ENUM_FIELDS.items(): + if json_field in model_dict: + model_kwargs[field_name] = _enum_value( + value=model_dict[json_field], + enum_type=enum_type, + path=f"{path}/{json_field}", + ) + + if "views" in model_dict: + views_items = _json_array( + value=model_dict["views"], + message=f"{path}/views must be an array.", + ) + model_kwargs["views"] = [ + _view_from_json(value=view_json, path=f"{path}/views({index})") + for index, view_json in enumerate(iterable=views_items) + ] + + return ModelTargetModel(**model_kwargs) + + +@beartype +def _models_from_file( + *, + models_file_path: Path, +) -> Sequence[ModelTargetModel]: + """Get the models described by a models file, or raise an error.""" + try: + file_json: object = json.loads(s=models_file_path.read_text()) + except json.JSONDecodeError as exc: + message = f"{models_file_path} is not valid JSON." + raise _models_file_error(message=message) from exc + + models_json: object = file_json + file_dict = _as_json_object(value=file_json) + if file_dict is not None: + if "models" not in file_dict: + message = "/models is required." + raise _models_file_error(message=message) + models_json = file_dict["models"] + + models_items = _json_array( + value=models_json, + message="/models must be an array.", + ) + return [ + _model_from_json(value=model_json, path=f"/models({index})") + for index, model_json in enumerate(iterable=models_items) + ] + + +@click.command(name="create-model-target-dataset") +@click.option( + "--name", + type=str, + required=True, + help="The name of the Model Target dataset.", +) +@click.option( + "--target-sdk", + type=str, + required=True, + help=( + "The Vuforia Engine version to generate the dataset for, for example " + '"10.29".' + ), +) +@dataset_type_option +@click.option( + "--model-name", + type=str, + help=( + "The name of the model to generate the dataset from. This is " + "required unless --models-file is given." + ), +) +@click.option( + "--cad-data-url", + type=str, + help="A URL which Vuforia downloads the model's CAD data from.", +) +@click.option( + "--cad-data-file", + "cad_data_file_path", + type=click.Path( + exists=True, + dir_okay=False, + path_type=Path, + ), + help=( + "The path to a file with the model's CAD data. The file is sent to " + "Vuforia as base64 encoded data." + ), +) +@click.option( + "--automatic-coloring", + type=click.Choice(choices=AutomaticColoring, case_sensitive=False), + help="Whether Vuforia colors the model automatically.", +) +@click.option( + "--cad-data-format", + type=click.Choice(choices=CadDataFormat, case_sensitive=False), + help="The format of the model's CAD data.", +) +@click.option( + "--motion-hint", + type=click.Choice(choices=MotionHint, case_sensitive=False), + help="How the object which the model represents moves.", +) +@click.option( + "--optimize-tracking-for", + type=click.Choice(choices=OptimizeTrackingFor, case_sensitive=False), + help="What Vuforia optimizes tracking of the model for.", +) +@click.option( + "--realistic-appearance", + type=click.Choice(choices=RealisticAppearance, case_sensitive=False), + help=( + "Whether the model looks like the object it represents. This is " + "documented for advanced datasets only." + ), +) +@click.option( + "--simplify", + type=click.Choice(choices=Simplify, case_sensitive=False), + help="Whether Vuforia simplifies the model.", +) +@click.option( + "--tracking-mode", + type=click.Choice(choices=TrackingMode, case_sensitive=False), + help="The tracking mode to generate the dataset for.", +) +@click.option( + "--state-based-configuration-file", + "state_based_configuration_file_path", + type=click.Path( + exists=True, + dir_okay=False, + path_type=Path, + ), + help=( + "The path to a JSON file with the model's State-Based Model Target " + "configuration." + ), +) +@click.option( + "--models-file", + "models_file_path", + type=click.Path( + exists=True, + dir_okay=False, + path_type=Path, + ), + help=( + "The path to a JSON file which describes the models to generate the " + "dataset from, in the form which the Model Target Web API takes. The " + "file holds either an array of models or an object with a " + '"models" array. This is the only way to give multiple models, or ' + "to give guide views, and it cannot be used with the other model " + "options." + ), +) +@client_id_option +@client_secret_option +@base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_model_target_exceptions() +@beartype +def create_model_target_dataset( + *, + client_id: str, + client_secret: str, + name: str, + target_sdk: str, + dataset_type: ModelTargetDatasetType, + model_name: str | None, + cad_data_url: str | None, + cad_data_file_path: Path | None, + automatic_coloring: AutomaticColoring | None, + cad_data_format: CadDataFormat | None, + motion_hint: MotionHint | None, + optimize_tracking_for: OptimizeTrackingFor | None, + realistic_appearance: RealisticAppearance | None, + simplify: Simplify | None, + tracking_mode: TrackingMode | None, + state_based_configuration_file_path: Path | None, + models_file_path: Path | None, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> None: + """Create a Model Target dataset. + + Vuforia generates the dataset in the background, so the dataset is not + available to download immediately. The UUID of the new dataset is shown. + + \b + See + https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + """ + model_option_values: dict[str, object] = { + "--automatic-coloring": automatic_coloring, + "--cad-data-file": cad_data_file_path, + "--cad-data-format": cad_data_format, + "--cad-data-url": cad_data_url, + "--model-name": model_name, + "--motion-hint": motion_hint, + "--optimize-tracking-for": optimize_tracking_for, + "--realistic-appearance": realistic_appearance, + "--simplify": simplify, + "--state-based-configuration-file": ( + state_based_configuration_file_path + ), + "--tracking-mode": tracking_mode, + } + + if models_file_path is not None: + given_model_options = sorted( + option_name + for option_name, value in model_option_values.items() + if value is not None + ) + if given_model_options: + message = ( + "--models-file cannot be used with " + f"{', '.join(given_model_options)}." + ) + raise click.UsageError(message=message) + models = _models_from_file(models_file_path=models_file_path) + else: + if model_name is None: + message = "--model-name is required when --models-file is not." + raise click.UsageError(message=message) + + if (cad_data_url is None) == (cad_data_file_path is None): + message = ( + "Exactly one of --cad-data-url and --cad-data-file is " + "required." + ) + raise click.UsageError(message=message) + + cad_data_blob: str | None = None + if cad_data_file_path is not None: + cad_data_blob = base64.b64encode( + s=cad_data_file_path.read_bytes(), + ).decode(encoding="ascii") + + state_based_configuration_json_string: str | None = None + if state_based_configuration_file_path is not None: + state_based_configuration_json_string = ( + state_based_configuration_file_path.read_text() + ) + + models = [ + ModelTargetModel( + name=model_name, + cad_data_url=cad_data_url, + cad_data_blob=cad_data_blob, + automatic_coloring=automatic_coloring, + cad_data_format=cad_data_format, + motion_hint=motion_hint, + optimize_tracking_for=optimize_tracking_for, + realistic_appearance=realistic_appearance, + simplify=simplify, + tracking_mode=tracking_mode, + state_based_configuration_json_string=( + state_based_configuration_json_string + ), + ), + ] + + model_target_client = _model_target_client( + client_id=client_id, + client_secret=client_secret, + base_vws_url=base_vws_url, + connection_timeout_seconds=connection_timeout_seconds, + read_timeout_seconds=read_timeout_seconds, + ) + + dataset_uuid = model_target_client.create_dataset( + name=name, + target_sdk=target_sdk, + models=models, + dataset_type=dataset_type, + ) + + click.echo(message=dataset_uuid) + + +@click.command(name="get-model-target-dataset-status") +@dataset_uuid_option +@dataset_type_option +@client_id_option +@client_secret_option +@base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_model_target_exceptions() +@beartype +def get_model_target_dataset_status( + *, + client_id: str, + client_secret: str, + dataset_uuid: str, + dataset_type: ModelTargetDatasetType, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> None: + """Get the status of a Model Target dataset. + + \b + See + https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + """ + model_target_client = _model_target_client( + client_id=client_id, + client_secret=client_secret, + base_vws_url=base_vws_url, + connection_timeout_seconds=connection_timeout_seconds, + read_timeout_seconds=read_timeout_seconds, + ) + + report = model_target_client.get_dataset_status( + dataset_uuid=dataset_uuid, + dataset_type=dataset_type, + ) + + click.echo(message=_status_report_yaml(report=report)) + + +@click.command(name="wait-for-model-target-dataset-generated") +@click.option( + "--seconds-between-requests", + type=click.FloatRange(min=0.05), + default=_SECONDS_BETWEEN_REQUESTS_DEFAULT, + help=_SECONDS_BETWEEN_REQUESTS_HELP, + show_default=True, +) +@click.option( + "--timeout-seconds", + type=click.FloatRange(min=0.05), + default=300, + help=_TIMEOUT_SECONDS_HELP, + show_default=True, +) +@dataset_uuid_option +@dataset_type_option +@client_id_option +@client_secret_option +@base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_model_target_exceptions() +@beartype +def wait_for_model_target_dataset_generated( + *, + client_id: str, + client_secret: str, + dataset_uuid: str, + dataset_type: ModelTargetDatasetType, + seconds_between_requests: float, + timeout_seconds: float, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> None: + """Wait for Vuforia to finish generating a Model Target dataset. + + This is done by polling the Model Target Web API. The status of the + dataset is shown once Vuforia has finished with it. A dataset which + failed to generate is also finished, and gives a non-zero exit code. + """ + model_target_client = _model_target_client( + client_id=client_id, + client_secret=client_secret, + base_vws_url=base_vws_url, + connection_timeout_seconds=connection_timeout_seconds, + read_timeout_seconds=read_timeout_seconds, + ) + + try: + report = model_target_client.wait_for_dataset_generated( + dataset_uuid=dataset_uuid, + dataset_type=dataset_type, + seconds_between_requests=seconds_between_requests, + timeout_seconds=timeout_seconds, + ) + except ModelTargetDatasetTimeoutError: + click.echo( + message=f"Timeout of {timeout_seconds} seconds reached.", + err=True, + ) + sys.exit(1) + + click.echo(message=_status_report_yaml(report=report)) + + if report.status == ModelTargetDatasetStatuses.FAILED: + click.echo( + message="Error: Vuforia failed to generate the dataset.", + err=True, + ) + sys.exit(1) + + +@click.command(name="download-model-target-dataset") +@click.option( + "--output", + "output_file_path", + type=click.Path( + dir_okay=False, + writable=True, + path_type=Path, + ), + required=True, + help="The path to write the generated dataset zip file to.", +) +@dataset_uuid_option +@dataset_type_option +@client_id_option +@client_secret_option +@base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_model_target_exceptions() +@beartype +def download_model_target_dataset( + *, + client_id: str, + client_secret: str, + dataset_uuid: str, + dataset_type: ModelTargetDatasetType, + output_file_path: Path, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> None: + """Download a generated Model Target dataset. + + \b + See + https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + """ + model_target_client = _model_target_client( + client_id=client_id, + client_secret=client_secret, + base_vws_url=base_vws_url, + connection_timeout_seconds=connection_timeout_seconds, + read_timeout_seconds=read_timeout_seconds, + ) + + dataset = model_target_client.download_dataset( + dataset_uuid=dataset_uuid, + dataset_type=dataset_type, + ) + + output_file_path.write_bytes(data=dataset) + + +@click.command(name="delete-model-target-dataset") +@dataset_uuid_option +@dataset_type_option +@client_id_option +@client_secret_option +@base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_model_target_exceptions() +@beartype +def delete_model_target_dataset( + *, + client_id: str, + client_secret: str, + dataset_uuid: str, + dataset_type: ModelTargetDatasetType, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> None: + """Delete a Model Target dataset. + + \b + See + https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + """ + model_target_client = _model_target_client( + client_id=client_id, + client_secret=client_secret, + base_vws_url=base_vws_url, + connection_timeout_seconds=connection_timeout_seconds, + read_timeout_seconds=read_timeout_seconds, + ) + + model_target_client.delete_dataset( + dataset_uuid=dataset_uuid, + dataset_type=dataset_type, + ) diff --git a/src/vws_cli/options/__init__.py b/src/vws_cli/options/__init__.py index e001e2d2..834d6fb5 100644 --- a/src/vws_cli/options/__init__.py +++ b/src/vws_cli/options/__init__.py @@ -1,3 +1 @@ -""" -Options for the VWS CLI commands. -""" +"""Options for the VWS CLI commands.""" diff --git a/src/vws_cli/options/credentials.py b/src/vws_cli/options/credentials.py index 246e87a6..353c7c8f 100644 --- a/src/vws_cli/options/credentials.py +++ b/src/vws_cli/options/credentials.py @@ -1,22 +1,18 @@ -""" -``click`` options regarding credentials. -""" +"""``click`` options regarding credentials.""" from collections.abc import Callable +from typing import Any import click +from beartype import beartype +@beartype def server_access_key_option( - command: Callable[..., None] | None = None, -) -> Callable[..., None]: - """ - An option decorator for the Vuforia server access key. - """ - click_option_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the Vuforia server access key.""" + return click.option( "--server-access-key", type=str, help=( @@ -26,22 +22,15 @@ def server_access_key_option( required=True, envvar="VUFORIA_SERVER_ACCESS_KEY", show_envvar=True, - ) - assert command is not None - function: Callable[..., None] = click_option_function(command) - return function + )(command) +@beartype def server_secret_key_option( - command: Callable[..., None] | None = None, -) -> Callable[..., None]: - """ - An option decorator for the Vuforia server secret key. - """ - click_option_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the Vuforia server secret key.""" + return click.option( "--server-secret-key", type=str, help=( @@ -51,22 +40,15 @@ def server_secret_key_option( required=True, envvar="VUFORIA_SERVER_SECRET_KEY", show_envvar=True, - ) - assert command is not None - function: Callable[..., None] = click_option_function(command) - return function + )(command) +@beartype def client_access_key_option( - command: Callable[..., None] | None = None, -) -> Callable[..., None]: - """ - An option decorator for the Vuforia client access key. - """ - click_option_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the Vuforia client access key.""" + return click.option( "--client-access-key", type=str, help=( @@ -76,22 +58,15 @@ def client_access_key_option( required=True, envvar="VUFORIA_CLIENT_ACCESS_KEY", show_envvar=True, - ) - assert command is not None - function: Callable[..., None] = click_option_function(command) - return function + )(command) +@beartype def client_secret_key_option( - command: Callable[..., None] | None = None, -) -> Callable[..., None]: - """ - An option decorator for the Vuforia client secret key. - """ - click_option_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the Vuforia client secret key.""" + return click.option( "--client-secret-key", type=str, help=( @@ -101,7 +76,4 @@ def client_secret_key_option( required=True, envvar="VUFORIA_CLIENT_SECRET_KEY", show_envvar=True, - ) - assert command is not None - function: Callable[..., None] = click_option_function(command) - return function + )(command) diff --git a/src/vws_cli/options/model_targets.py b/src/vws_cli/options/model_targets.py new file mode 100644 index 00000000..b04dc2ef --- /dev/null +++ b/src/vws_cli/options/model_targets.py @@ -0,0 +1,81 @@ +"""``click`` options regarding Model Target datasets.""" + +from collections.abc import Callable +from typing import Any + +import click +from beartype import beartype +from vws.model_target_datasets import ModelTargetDatasetType + + +@beartype +def client_id_option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the Model Target Web API client ID.""" + return click.option( + "--client-id", + type=str, + help=( + "A Vuforia OAuth2 client ID to use to access the Model Target " + "Web API." + ), + required=True, + envvar="VUFORIA_MODEL_TARGET_CLIENT_ID", + show_envvar=True, + )(command) + + +@beartype +def client_secret_option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the Model Target Web API client secret.""" + return click.option( + "--client-secret", + type=str, + help=( + "A Vuforia OAuth2 client secret to use to access the Model " + "Target Web API." + ), + required=True, + envvar="VUFORIA_MODEL_TARGET_CLIENT_SECRET", + show_envvar=True, + )(command) + + +@beartype +def dataset_type_option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the kind of Model Target dataset.""" + return click.option( + "--dataset-type", + type=click.Choice( + choices=ModelTargetDatasetType, + case_sensitive=False, + ), + default=ModelTargetDatasetType.STANDARD.value, + help=( + "The kind of Model Target dataset. Standard and advanced " + "datasets are separate resources, so a dataset created as one " + "type is not visible to requests for the other type." + ), + show_default=True, + )(command) + + +@beartype +def dataset_uuid_option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the UUID of a Model Target dataset.""" + return click.option( + "--dataset-uuid", + type=str, + help=( + "The UUID of a Model Target dataset, as given when the dataset " + "was created." + ), + required=True, + )(command) diff --git a/src/vws_cli/options/targets.py b/src/vws_cli/options/targets.py index 91aa2f12..884dee67 100644 --- a/src/vws_cli/options/targets.py +++ b/src/vws_cli/options/targets.py @@ -1,6 +1,4 @@ -""" -``click`` options regarding targets. -""" +"""``click`` options regarding targets.""" from collections.abc import Callable from enum import Enum, unique @@ -10,7 +8,7 @@ import click from beartype import beartype -target_id_option: Callable[..., None] = click.option( +target_id_option: Callable[..., Any] = click.option( "--target-id", type=str, help="The ID of a target in the Vuforia database.", @@ -19,10 +17,8 @@ @beartype -def target_name_option(*, required: bool) -> Callable[..., None]: - """ - An option decorator for choosing a target name. - """ +def target_name_option(*, required: bool) -> Callable[..., Any]: + """An option decorator for choosing a target name.""" return click.option( "--name", type=str, @@ -33,9 +29,7 @@ def target_name_option(*, required: bool) -> Callable[..., None]: @beartype def target_width_option(*, required: bool) -> Callable[..., Any]: - """ - An option decorator for choosing a target width. - """ + """An option decorator for choosing a target width.""" option: Callable[..., Any] = click.option( "--width", type=float, @@ -47,9 +41,7 @@ def target_width_option(*, required: bool) -> Callable[..., Any]: @beartype def target_image_option(*, required: bool) -> Callable[..., Any]: - """ - An option decorator for choosing a target image. - """ + """An option decorator for choosing a target image.""" return click.option( "--image", "image_file_path", @@ -66,39 +58,18 @@ def target_image_option(*, required: bool) -> Callable[..., Any]: @unique class ActiveFlagChoice(Enum): - """ - Choices for active flag. - """ + """Choices for active flag.""" TRUE = "true" FALSE = "false" -def _active_flag_choice_callback( - ctx: click.core.Context, - param: click.core.Option | click.core.Parameter, - value: str | None, -) -> ActiveFlagChoice | None: - """ - Use as a callback for active flag options. - """ - # This is to satisfy the "vulture" linter. - del ctx - del param - - if value is None: - return None - - return ActiveFlagChoice(value=value) - - +@beartype def active_flag_option( *, allow_none: bool, -) -> Callable[..., None]: - """ - An option decorator for setting a target's active flag. - """ +) -> Callable[..., Any]: + """An option decorator for setting a target's active flag.""" if allow_none: default = None show_default = False @@ -110,14 +81,13 @@ def active_flag_option( "--active-flag", "active_flag_choice", help="Whether or not the target is active for query.", - type=click.Choice(choices=[item.value for item in ActiveFlagChoice]), + type=click.Choice(choices=ActiveFlagChoice, case_sensitive=False), default=default, - callback=_active_flag_choice_callback, show_default=show_default, ) -application_metadata_option: Callable[..., None] = click.option( +application_metadata_option: Callable[..., Any] = click.option( "--application-metadata", type=str, required=False, diff --git a/src/vws_cli/options/timeout.py b/src/vws_cli/options/timeout.py new file mode 100644 index 00000000..bf8bfad3 --- /dev/null +++ b/src/vws_cli/options/timeout.py @@ -0,0 +1,35 @@ +"""``click`` options regarding timeouts.""" + +from collections.abc import Callable +from typing import Any + +import click +from beartype import beartype + + +@beartype +def connection_timeout_seconds_option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the connection timeout.""" + return click.option( + "--connection-timeout-seconds", + type=click.FloatRange(min=0.05), + default=30, + help="The connection timeout for HTTP requests, in seconds.", + show_default=True, + )(command) + + +@beartype +def read_timeout_seconds_option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the read timeout.""" + return click.option( + "--read-timeout-seconds", + type=click.FloatRange(min=0.05), + default=30, + help="The read timeout for HTTP requests, in seconds.", + show_default=True, + )(command) diff --git a/src/vws_cli/options/vws.py b/src/vws_cli/options/vws.py new file mode 100644 index 00000000..c944d58d --- /dev/null +++ b/src/vws_cli/options/vws.py @@ -0,0 +1,39 @@ +"""``click`` options for VWS API options.""" + +from collections.abc import Callable +from typing import Any + +import click +from beartype import beartype + + +@beartype +def database_id_option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for the Vuforia database ID.""" + return click.option( + "--database-id", + type=str, + help=( + "The ID of the Vuforia database which the given server keys " + "belong to. This is shown in the Vuforia target manager." + ), + required=True, + envvar="VUFORIA_DATABASE_ID", + show_envvar=True, + )(command) + + +@beartype +def base_vws_url_option( + command: Callable[..., Any], +) -> Callable[..., Any]: + """An option decorator for choosing the base VWS URL.""" + return click.option( + "--base-vws-url", + type=click.STRING, + default="https://vws.vuforia.com", + help="The base URL for the VWS API.", + show_default=True, + )(command) diff --git a/src/vws_cli/query.py b/src/vws_cli/query.py index 29c27d96..e1648b77 100644 --- a/src/vws_cli/query.py +++ b/src/vws_cli/query.py @@ -1,14 +1,11 @@ -""" -A CLI for the Vuforia Cloud Recognition Service API. -""" +"""A CLI for the Vuforia Cloud Recognition Service API.""" import contextlib import dataclasses import io import sys -from collections.abc import Callable, Iterator +from collections.abc import Generator from pathlib import Path -from typing import Any import click import yaml @@ -30,14 +27,16 @@ client_access_key_option, client_secret_key_option, ) +from vws_cli.options.timeout import ( + connection_timeout_seconds_option, + read_timeout_seconds_option, +) @beartype @contextlib.contextmanager -def handle_vwq_exceptions() -> Iterator[None]: - """ - Show error messages and catch exceptions from ``VWS-Python``. - """ +def _handle_vwq_exceptions() -> Generator[None]: + """Show error messages and catch exceptions from ``VWS-Python``.""" try: yield except BadImageError: @@ -66,127 +65,64 @@ def handle_vwq_exceptions() -> Iterator[None]: sys.exit(1) -@beartype -def image_argument(command: Callable[..., None]) -> Callable[..., Any]: - """ - An argument decorator for choosing a query image. - """ - click_argument_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.argument( - "image", - type=click.Path( - exists=True, - file_okay=True, - dir_okay=False, - path_type=Path, - ), - ) - - function: Callable[..., None] = click_argument_function(command) - return function - - -def max_num_results_option( - command: Callable[..., None], -) -> Callable[..., None]: - """ - An option decorator for choosing the maximum number of query results. - """ - maximum = 50 - click_option_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.option( - "--max-num-results", - type=click.IntRange(min=1, max=maximum), - default=1, - help=( - "The maximum number of matching targets to be returned. " - f"Must be <= {maximum}." - ), - show_default=True, - ) - - function: Callable[..., None] = click_option_function(command) - return function - - -def include_target_data_callback( - ctx: click.core.Context, - param: click.core.Option | click.core.Parameter, - value: str, -) -> CloudRecoIncludeTargetData: - """ - Use as a callback for active flag options. - """ - # This is to satisfy the "vulture" linter. - del ctx - del param - - return { - "top": CloudRecoIncludeTargetData.TOP, - "none": CloudRecoIncludeTargetData.NONE, - "all": CloudRecoIncludeTargetData.ALL, - }[value] - - -def include_target_data_option( - command: Callable[..., None], -) -> Callable[..., None]: - """ - An option decorator for choosing whether to include target data. - """ - click_option_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.option( - "--include-target-data", - type=click.Choice(choices=["top", "none", "all"], case_sensitive=True), - default="top", - callback=include_target_data_callback, - help=( - "Whether target_data records shall be returned for the matched " - "targets. Accepted values are top (default value, only return " - "target_data for top ranked match), none (return no target_data), " - "all (for all matched targets)." - ), - show_default=True, - ) +_image_argument = click.argument( + "image", + type=click.Path( + exists=True, + file_okay=True, + dir_okay=False, + path_type=Path, + ), +) - function: Callable[..., None] = click_option_function(command) - return function +_MAX_NUM_RESULTS_DEFAULT = 50 +_max_num_results_option = click.option( + "--max-num-results", + type=click.IntRange(min=1, max=_MAX_NUM_RESULTS_DEFAULT), + default=1, + help=( + "The maximum number of matching targets to be returned. " + f"Must be <= {_MAX_NUM_RESULTS_DEFAULT}." + ), + show_default=True, +) -@beartype -def base_vwq_url_option(command: Callable[..., None]) -> Callable[..., None]: - """ - An option decorator for choosing the base VWQ URL. - """ - click_option_function: Callable[ - [Callable[..., None]], - Callable[..., None], - ] = click.option( - "--base-vwq-url", - type=click.STRING, - default="https://cloudreco.vuforia.com", - help="The base URL for the VWQ API.", - show_default=True, - ) +_include_target_data_option = click.option( + "--include-target-data", + type=click.Choice( + choices=CloudRecoIncludeTargetData, + case_sensitive=False, + ), + default=CloudRecoIncludeTargetData.TOP.lower(), + help=( + "Whether target_data records shall be returned for the matched " + "targets. Accepted values are top (default value, only return " + "target_data for top ranked match), none (return no target_data), " + "all (for all matched targets)." + ), + show_default=True, +) - function: Callable[..., None] = click_option_function(command) - return function +_base_vwq_url_option = click.option( + "--base-vwq-url", + type=click.STRING, + default="https://cloudreco.vuforia.com", + help="The base URL for the VWQ API.", + show_default=True, +) @click.command(name="vuforia-cloud-reco") -@image_argument +@_image_argument @client_access_key_option @client_secret_key_option -@include_target_data_option -@max_num_results_option -@base_vwq_url_option -@handle_vwq_exceptions() +@_include_target_data_option +@_max_num_results_option +@_base_vwq_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@_handle_vwq_exceptions() # We set the ``version`` parameter because in PyInstaller binaries, # ``pkg_resources`` is not available. # @@ -201,14 +137,18 @@ def vuforia_cloud_reco( max_num_results: int, include_target_data: CloudRecoIncludeTargetData, base_vwq_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, ) -> None: - """ - Make a request to the Vuforia Cloud Recognition Service API. - """ + """Make a request to the Vuforia Cloud Recognition Service API.""" client = CloudRecoService( client_access_key=client_access_key, client_secret_key=client_secret_key, base_vwq_url=base_vwq_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), ) query_result = client.query( image=io.BytesIO(initial_bytes=image.read_bytes()), diff --git a/src/vws_cli/vumark.py b/src/vws_cli/vumark.py new file mode 100644 index 00000000..8fadb8f1 --- /dev/null +++ b/src/vws_cli/vumark.py @@ -0,0 +1,156 @@ +"""``click`` command for VuMark generation.""" + +import contextlib +import sys +from collections.abc import Generator +from enum import StrEnum, unique +from pathlib import Path + +import click +from beartype import beartype +from vws import VuMarkService +from vws.exceptions.base_exceptions import VWSError +from vws.exceptions.custom_exceptions import ServerError +from vws.exceptions.vws_exceptions import ( + InvalidInstanceIdError, + TargetStatusNotSuccessError, + UnknownTargetError, +) +from vws.vumark_accept import VuMarkAccept + +from vws_cli import __version__ +from vws_cli._error_handling import get_error_message +from vws_cli.options.credentials import ( + server_access_key_option, + server_secret_key_option, +) +from vws_cli.options.targets import target_id_option +from vws_cli.options.timeout import ( + connection_timeout_seconds_option, + read_timeout_seconds_option, +) +from vws_cli.options.vws import base_vws_url_option + + +@beartype +@unique +class VuMarkFormatChoice(StrEnum): + """Choices for the VuMark output format.""" + + PNG = "png" + SVG = "svg" + PDF = "pdf" + + +_FORMAT_CHOICE_TO_ACCEPT: dict[VuMarkFormatChoice, VuMarkAccept] = { + VuMarkFormatChoice.PNG: VuMarkAccept.PNG, + VuMarkFormatChoice.SVG: VuMarkAccept.SVG, + VuMarkFormatChoice.PDF: VuMarkAccept.PDF, +} + + +@beartype +@contextlib.contextmanager +def _handle_vumark_exceptions() -> Generator[None]: + """Show error messages and catch exceptions from ``VWS-Python``.""" + error_message = "" + + try: + yield + except (UnknownTargetError, TargetStatusNotSuccessError) as exc: + match exc: + case UnknownTargetError(): + error_message = ( + f'Error: Target "{exc.target_id}" does not exist.' + ) + case _: + error_message = ( + f'Error: The target "{exc.target_id}" is not in the ' + "success state and cannot be used to generate a " + "VuMark instance." + ) + except (VWSError, ServerError) as exc: + match exc: + case InvalidInstanceIdError(): + error_message = "Error: The given instance ID is invalid." + case _: + error_message = get_error_message(exc=exc) + else: + return + + click.echo(message=error_message, err=True) + sys.exit(1) + + +@click.command(name="vumark") +@server_access_key_option +@server_secret_key_option +@target_id_option +@click.option( + "--instance-id", + type=str, + required=True, + help="The instance ID to encode in the VuMark.", +) +@click.option( + "--format", + "format_choice", + type=click.Choice(choices=VuMarkFormatChoice, case_sensitive=False), + default=VuMarkFormatChoice.PNG.lower(), + help="The output format for the generated VuMark.", + show_default=True, +) +@click.option( + "--output", + "output_file_path", + type=click.Path( + dir_okay=False, + writable=True, + path_type=Path, + ), + required=True, + help="The path to write the generated VuMark to.", +) +@_handle_vumark_exceptions() +@base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@click.version_option(version=__version__) +@beartype +def generate_vumark( + *, + server_access_key: str, + server_secret_key: str, + target_id: str, + instance_id: str, + format_choice: VuMarkFormatChoice, + output_file_path: Path, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> None: + """Generate a VuMark instance. + + \b + See + https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/ + """ + vumark_client = VuMarkService( + server_access_key=server_access_key, + server_secret_key=server_secret_key, + base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), + ) + + accept = _FORMAT_CHOICE_TO_ACCEPT[format_choice] + + vumark_data = vumark_client.generate_vumark_instance( + target_id=target_id, + instance_id=instance_id, + accept=accept, + ) + + output_file_path.write_bytes(data=vumark_data) diff --git a/tests/__init__.py b/tests/__init__.py index 76356810..fb30f6f7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1 @@ -""" -Tests for the VWS CLI. -""" +"""Tests for the VWS CLI.""" diff --git a/tests/conftest.py b/tests/conftest.py index 080c3d12..836a94d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,54 +1,56 @@ -""" -``pytest`` fixtures. -""" +"""``pytest`` fixtures.""" from collections.abc import Iterator import pytest -from beartype import beartype from mock_vws import MockVWS -from mock_vws.database import VuforiaDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase +from mock_vws.target import VuMarkTarget from vws import VWS, CloudRecoService -@beartype -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - """ - Apply the beartype decorator to all collected test functions. - """ - for item in items: - assert isinstance(item, pytest.Function) - item.obj = beartype(obj=item.obj) - - @pytest.fixture(name="mock_database") -def fixture_mock_database() -> Iterator[VuforiaDatabase]: - """ - Yield a mock ``VuforiaDatabase``. - """ +def fixture_mock_database() -> Iterator[CloudDatabase]: + """Yield a mock ``CloudDatabase``.""" with MockVWS() as mock: - database = VuforiaDatabase() - mock.add_database(database=database) + database = CloudDatabase() + mock.add_cloud_database(cloud_database=database) yield database @pytest.fixture -def vws_client(mock_database: VuforiaDatabase) -> VWS: - """ - Return a VWS client which connects to a mock database. - """ +def vws_client(*, mock_database: CloudDatabase) -> VWS: + """Return a VWS client which connects to a mock database.""" return VWS( server_access_key=mock_database.server_access_key, server_secret_key=mock_database.server_secret_key, ) +@pytest.fixture(name="vumark_database") +def fixture_vumark_database() -> Iterator[VuMarkDatabase]: + """Yield a mock ``VuMarkDatabase`` with one pre-created target.""" + vumark_target = VuMarkTarget(name="test-vumark-target") + database = VuMarkDatabase(vumark_targets={vumark_target}) + with MockVWS() as mock: + mock.add_vumark_database(vumark_database=database) + yield database + + +@pytest.fixture(name="vumark_target") +def fixture_vumark_target(*, vumark_database: VuMarkDatabase) -> VuMarkTarget: + """Return the pre-created ``VuMarkTarget`` in the database.""" + return next(iter(vumark_database.not_deleted_targets)) + + @pytest.fixture def cloud_reco_client( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, ) -> CloudRecoService: """ - Return a ``CloudRecoService`` client which connects to a mock database. + Return a ``CloudRecoService`` client which connects to a mock + database. """ return CloudRecoService( client_access_key=mock_database.client_access_key, diff --git a/tests/test_cli.py b/tests/test_cli.py index f830dd46..a8d726d6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,4 @@ -""" -Tests for the VWS CLI. -""" +"""Tests for the VWS CLI.""" from click.testing import CliRunner @@ -8,9 +6,7 @@ def test_version() -> None: - """ - The CLI version is shown with ``vws --version``. - """ + """The CLI version is shown with ``vws --version``.""" runner = CliRunner() result = runner.invoke( cli=vws_group, diff --git a/tests/test_help.py b/tests/test_help.py index 9d782a01..a9e8647d 100644 --- a/tests/test_help.py +++ b/tests/test_help.py @@ -1,6 +1,4 @@ -""" -Tests for the VWS CLI help. -""" +"""Tests for the VWS CLI help.""" import pytest from click.testing import CliRunner @@ -8,6 +6,7 @@ from vws_cli import vws_group from vws_cli.query import vuforia_cloud_reco +from vws_cli.vumark import generate_vumark _SUBCOMMANDS = [[item] for item in vws_group.commands] _BASE_COMMAND: list[list[str]] = [[]] @@ -20,6 +19,7 @@ ids=[str(object=cmd) for cmd in _COMMANDS], ) def test_vws_command_help( + *, command: list[str], file_regression: FileRegressionFixture, ) -> None: @@ -40,6 +40,24 @@ def test_vws_command_help( file_regression.check(contents=result.output) +def test_vumark_help(file_regression: FileRegressionFixture) -> None: + """Expected help text is shown for the ``vumark`` command. + + This help text is defined in files. + To update these files, run ``pytest`` with the ``--regen-all`` flag. + """ + runner = CliRunner() + arguments = ["--help"] + result = runner.invoke( + cli=generate_vumark, + args=arguments, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + file_regression.check(contents=result.output) + + def test_query_help(file_regression: FileRegressionFixture) -> None: """Expected help text is shown for the ``vuforia-cloud-reco`` command. diff --git a/tests/test_help/test_query_help.txt b/tests/test_help/test_query_help.txt index 21f3cd7f..713550da 100644 --- a/tests/test_help/test_query_help.txt +++ b/tests/test_help/test_query_help.txt @@ -22,5 +22,11 @@ Options: 1<=x<=50] --base-vwq-url TEXT The base URL for the VWQ API. [default: https://cloudreco.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] --version Show the version and exit. --help Show this message and exit. diff --git a/tests/test_help/test_vumark_help.txt b/tests/test_help/test_vumark_help.txt new file mode 100644 index 00000000..d5ab6ba8 --- /dev/null +++ b/tests/test_help/test_vumark_help.txt @@ -0,0 +1,32 @@ +Usage: vumark [OPTIONS] + + Generate a VuMark instance. + + See + https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/ + +Options: + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --target-id TEXT The ID of a target in the Vuforia database. + [required] + --instance-id TEXT The instance ID to encode in the VuMark. + [required] + --format [png|svg|pdf] The output format for the generated VuMark. + [default: png] + --output FILE The path to write the generated VuMark to. + [required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --version Show the version and exit. + --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help____.txt b/tests/test_help/test_vws_command_help____.txt index 13ff9f18..c757a12b 100644 --- a/tests/test_help/test_vws_command_help____.txt +++ b/tests/test_help/test_vws_command_help____.txt @@ -7,12 +7,21 @@ Options: -h, --help Show this message and exit. Commands: - add-target Add a target. - delete-target Delete a target. - get-database-summary-report Get a database summary report. - get-duplicate-targets Get a list of potential duplicate targets. - get-target-record Get a target record. - get-target-summary-report Get a target summary report. - list-targets List targets. - update-target Update a target. - wait-for-target-processed Wait for a target to be "processed". + add-target Add a target. + create-model-target-dataset Create a Model Target dataset. + delete-model-target-dataset Delete a Model Target dataset. + delete-target Delete a target. + download-model-target-dataset Download a generated Model... + get-database-reco-counts-report + Get a per-target recognition... + get-database-summary-report Get a database summary report. + get-duplicate-targets Get a list of potential... + get-model-target-dataset-status + Get the status of a Model Target... + get-target-record Get a target record. + get-target-summary-report Get a target summary report. + list-targets List targets. + update-target Update a target. + wait-for-model-target-dataset-generated + Wait for Vuforia to finish... + wait-for-target-processed Wait for a target to be... diff --git a/tests/test_help/test_vws_command_help___add_target___.txt b/tests/test_help/test_vws_command_help___add_target___.txt index 02446251..d7b4c9ad 100644 --- a/tests/test_help/test_vws_command_help___add_target___.txt +++ b/tests/test_help/test_vws_command_help___add_target___.txt @@ -6,22 +6,28 @@ Usage: vws add-target [OPTIONS] https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#add Options: - --width FLOAT The width of the target in the Vuforia database. - [required] - --application-metadata TEXT The base64 encoded application metadata - associated with the target. - --server-access-key TEXT A Vuforia server access key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_ACCESS_KEY; required] - --server-secret-key TEXT A Vuforia server secret key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_SECRET_KEY; required] - --name TEXT The name of the target in the Vuforia database. - [required] - --image FILE The path to an image to upload and set as the - target image. [required] - --active-flag [true|false] Whether or not the target is active for query. - [default: true] - --base-vws-url TEXT The base URL for the VWS API. [default: - https://vws.vuforia.com] - -h, --help Show this message and exit. + --width FLOAT The width of the target in the Vuforia + database. [required] + --application-metadata TEXT The base64 encoded application metadata + associated with the target. + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --name TEXT The name of the target in the Vuforia + database. [required] + --image FILE The path to an image to upload and set as the + target image. [required] + --active-flag [true|false] Whether or not the target is active for query. + [default: true] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___create_model_target_dataset___.txt b/tests/test_help/test_vws_command_help___create_model_target_dataset___.txt new file mode 100644 index 00000000..8654c49a --- /dev/null +++ b/tests/test_help/test_vws_command_help___create_model_target_dataset___.txt @@ -0,0 +1,73 @@ +Usage: vws create-model-target-dataset [OPTIONS] + + Create a Model Target dataset. + + Vuforia generates the dataset in the background, so the dataset is not + available to download immediately. The UUID of the new dataset is shown. + + See + https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + +Options: + --name TEXT The name of the Model Target dataset. + [required] + --target-sdk TEXT The Vuforia Engine version to generate the + dataset for, for example "10.29". [required] + --dataset-type [standard|advanced] + The kind of Model Target dataset. Standard and + advanced datasets are separate resources, so a + dataset created as one type is not visible to + requests for the other type. [default: + standard] + --model-name TEXT The name of the model to generate the dataset + from. This is required unless --models-file is + given. + --cad-data-url TEXT A URL which Vuforia downloads the model's CAD + data from. + --cad-data-file FILE The path to a file with the model's CAD data. + The file is sent to Vuforia as base64 encoded + data. + --automatic-coloring [always|auto|never] + Whether Vuforia colors the model + automatically. + --cad-data-format [dae|fbx|glb|iges|obj|pvz|stl|vrml|zip] + The format of the model's CAD data. + --motion-hint [adaptive|dynamic|static] + How the object which the model represents + moves. + --optimize-tracking-for [ar_controller|default|low_feature_objects] + What Vuforia optimizes tracking of the model + for. + --realistic-appearance [auto|false|true] + Whether the model looks like the object it + represents. This is documented for advanced + datasets only. + --simplify [always|auto|never] Whether Vuforia simplifies the model. + --tracking-mode [car|default|scan] + The tracking mode to generate the dataset for. + --state-based-configuration-file FILE + The path to a JSON file with the model's + State-Based Model Target configuration. + --models-file FILE The path to a JSON file which describes the + models to generate the dataset from, in the + form which the Model Target Web API takes. The + file holds either an array of models or an + object with a "models" array. This is the only + way to give multiple models, or to give guide + views, and it cannot be used with the other + model options. + --client-id TEXT A Vuforia OAuth2 client ID to use to access + the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_ID; required] + --client-secret TEXT A Vuforia OAuth2 client secret to use to + access the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_SECRET; required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___delete_model_target_dataset___.txt b/tests/test_help/test_vws_command_help___delete_model_target_dataset___.txt new file mode 100644 index 00000000..a1c91d10 --- /dev/null +++ b/tests/test_help/test_vws_command_help___delete_model_target_dataset___.txt @@ -0,0 +1,31 @@ +Usage: vws delete-model-target-dataset [OPTIONS] + + Delete a Model Target dataset. + + See + https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + +Options: + --dataset-uuid TEXT The UUID of a Model Target dataset, as given + when the dataset was created. [required] + --dataset-type [standard|advanced] + The kind of Model Target dataset. Standard and + advanced datasets are separate resources, so a + dataset created as one type is not visible to + requests for the other type. [default: + standard] + --client-id TEXT A Vuforia OAuth2 client ID to use to access + the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_ID; required] + --client-secret TEXT A Vuforia OAuth2 client secret to use to + access the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_SECRET; required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___delete_target___.txt b/tests/test_help/test_vws_command_help___delete_target___.txt index 472b6d30..8862a48a 100644 --- a/tests/test_help/test_vws_command_help___delete_target___.txt +++ b/tests/test_help/test_vws_command_help___delete_target___.txt @@ -6,14 +6,20 @@ Usage: vws delete-target [OPTIONS] https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#delete. Options: - --server-access-key TEXT A Vuforia server access key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_ACCESS_KEY; required] - --server-secret-key TEXT A Vuforia server secret key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_SECRET_KEY; required] - --target-id TEXT The ID of a target in the Vuforia database. - [required] - --base-vws-url TEXT The base URL for the VWS API. [default: - https://vws.vuforia.com] - -h, --help Show this message and exit. + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --target-id TEXT The ID of a target in the Vuforia database. + [required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___download_model_target_dataset___.txt b/tests/test_help/test_vws_command_help___download_model_target_dataset___.txt new file mode 100644 index 00000000..ec05b8db --- /dev/null +++ b/tests/test_help/test_vws_command_help___download_model_target_dataset___.txt @@ -0,0 +1,33 @@ +Usage: vws download-model-target-dataset [OPTIONS] + + Download a generated Model Target dataset. + + See + https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + +Options: + --output FILE The path to write the generated dataset zip + file to. [required] + --dataset-uuid TEXT The UUID of a Model Target dataset, as given + when the dataset was created. [required] + --dataset-type [standard|advanced] + The kind of Model Target dataset. Standard and + advanced datasets are separate resources, so a + dataset created as one type is not visible to + requests for the other type. [default: + standard] + --client-id TEXT A Vuforia OAuth2 client ID to use to access + the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_ID; required] + --client-secret TEXT A Vuforia OAuth2 client secret to use to + access the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_SECRET; required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___get_database_reco_counts_report___.txt b/tests/test_help/test_vws_command_help___get_database_reco_counts_report___.txt new file mode 100644 index 00000000..2cd38898 --- /dev/null +++ b/tests/test_help/test_vws_command_help___get_database_reco_counts_report___.txt @@ -0,0 +1,50 @@ +Usage: vws get-database-reco-counts-report [OPTIONS] + + Get a per-target recognition counts report for a database. + + The report is a CSV with a ``target_id,reco_count`` header. Vuforia generates + the report in the background, so by default we wait for the report to be + generated before downloading it. + + See + https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api. + +Options: + --month TEXT The month to get recognition counts for, in + the YYYY-mm form. Vuforia accepts only the + current month and the previous month. + [default: (the current month)] + --output FILE The path to write the CSV report to. By + default, the report is written to stdout. + --no-wait Do not wait for the report to be generated. + Instead, show the URL to download the report + from once it has been generated. + --seconds-between-requests FLOAT RANGE + The number of seconds to wait between requests + made while polling for the report. We wait 0.2 + seconds by default, rather than less, than + that to decrease the number of calls made to + the API, to decrease the likelihood of hitting + the request quota. [default: 0.2; x>=0.05] + --timeout-seconds FLOAT RANGE The maximum number of seconds to wait for the + report to be generated. [default: 300; + x>=0.05] + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --database-id TEXT The ID of the Vuforia database which the given + server keys belong to. This is shown in the + Vuforia target manager. [env var: + VUFORIA_DATABASE_ID; required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___get_database_summary_report___.txt b/tests/test_help/test_vws_command_help___get_database_summary_report___.txt index 959d6a22..e6f42a6a 100644 --- a/tests/test_help/test_vws_command_help___get_database_summary_report___.txt +++ b/tests/test_help/test_vws_command_help___get_database_summary_report___.txt @@ -6,12 +6,18 @@ Usage: vws get-database-summary-report [OPTIONS] https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#summary-report. Options: - --server-access-key TEXT A Vuforia server access key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_ACCESS_KEY; required] - --server-secret-key TEXT A Vuforia server secret key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_SECRET_KEY; required] - --base-vws-url TEXT The base URL for the VWS API. [default: - https://vws.vuforia.com] - -h, --help Show this message and exit. + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___get_duplicate_targets___.txt b/tests/test_help/test_vws_command_help___get_duplicate_targets___.txt index b378f3c5..4e406a48 100644 --- a/tests/test_help/test_vws_command_help___get_duplicate_targets___.txt +++ b/tests/test_help/test_vws_command_help___get_duplicate_targets___.txt @@ -6,14 +6,20 @@ Usage: vws get-duplicate-targets [OPTIONS] https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#check. Options: - --server-access-key TEXT A Vuforia server access key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_ACCESS_KEY; required] - --server-secret-key TEXT A Vuforia server secret key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_SECRET_KEY; required] - --target-id TEXT The ID of a target in the Vuforia database. - [required] - --base-vws-url TEXT The base URL for the VWS API. [default: - https://vws.vuforia.com] - -h, --help Show this message and exit. + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --target-id TEXT The ID of a target in the Vuforia database. + [required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___get_model_target_dataset_status___.txt b/tests/test_help/test_vws_command_help___get_model_target_dataset_status___.txt new file mode 100644 index 00000000..245d6a95 --- /dev/null +++ b/tests/test_help/test_vws_command_help___get_model_target_dataset_status___.txt @@ -0,0 +1,31 @@ +Usage: vws get-model-target-dataset-status [OPTIONS] + + Get the status of a Model Target dataset. + + See + https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/ + +Options: + --dataset-uuid TEXT The UUID of a Model Target dataset, as given + when the dataset was created. [required] + --dataset-type [standard|advanced] + The kind of Model Target dataset. Standard and + advanced datasets are separate resources, so a + dataset created as one type is not visible to + requests for the other type. [default: + standard] + --client-id TEXT A Vuforia OAuth2 client ID to use to access + the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_ID; required] + --client-secret TEXT A Vuforia OAuth2 client secret to use to + access the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_SECRET; required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___get_target_record___.txt b/tests/test_help/test_vws_command_help___get_target_record___.txt index 173b6deb..1896cd69 100644 --- a/tests/test_help/test_vws_command_help___get_target_record___.txt +++ b/tests/test_help/test_vws_command_help___get_target_record___.txt @@ -6,14 +6,20 @@ Usage: vws get-target-record [OPTIONS] https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#target-record. Options: - --server-access-key TEXT A Vuforia server access key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_ACCESS_KEY; required] - --server-secret-key TEXT A Vuforia server secret key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_SECRET_KEY; required] - --target-id TEXT The ID of a target in the Vuforia database. - [required] - --base-vws-url TEXT The base URL for the VWS API. [default: - https://vws.vuforia.com] - -h, --help Show this message and exit. + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --target-id TEXT The ID of a target in the Vuforia database. + [required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___get_target_summary_report___.txt b/tests/test_help/test_vws_command_help___get_target_summary_report___.txt index 588ba302..1ec2e61c 100644 --- a/tests/test_help/test_vws_command_help___get_target_summary_report___.txt +++ b/tests/test_help/test_vws_command_help___get_target_summary_report___.txt @@ -6,14 +6,20 @@ Usage: vws get-target-summary-report [OPTIONS] https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#retrieve-report. Options: - --server-access-key TEXT A Vuforia server access key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_ACCESS_KEY; required] - --server-secret-key TEXT A Vuforia server secret key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_SECRET_KEY; required] - --target-id TEXT The ID of a target in the Vuforia database. - [required] - --base-vws-url TEXT The base URL for the VWS API. [default: - https://vws.vuforia.com] - -h, --help Show this message and exit. + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --target-id TEXT The ID of a target in the Vuforia database. + [required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___list_targets___.txt b/tests/test_help/test_vws_command_help___list_targets___.txt index 358068ff..9df9f166 100644 --- a/tests/test_help/test_vws_command_help___list_targets___.txt +++ b/tests/test_help/test_vws_command_help___list_targets___.txt @@ -6,12 +6,18 @@ Usage: vws list-targets [OPTIONS] https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#details-list. Options: - --server-access-key TEXT A Vuforia server access key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_ACCESS_KEY; required] - --server-secret-key TEXT A Vuforia server secret key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_SECRET_KEY; required] - --base-vws-url TEXT The base URL for the VWS API. [default: - https://vws.vuforia.com] - -h, --help Show this message and exit. + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___update_target___.txt b/tests/test_help/test_vws_command_help___update_target___.txt index c756d7b8..032a1567 100644 --- a/tests/test_help/test_vws_command_help___update_target___.txt +++ b/tests/test_help/test_vws_command_help___update_target___.txt @@ -6,21 +6,29 @@ Usage: vws update-target [OPTIONS] https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#update Options: - --width FLOAT The width of the target in the Vuforia database. - --application-metadata TEXT The base64 encoded application metadata - associated with the target. - --server-access-key TEXT A Vuforia server access key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_ACCESS_KEY; required] - --server-secret-key TEXT A Vuforia server secret key to use to access the - Vuforia Web Services API. [env var: - VUFORIA_SERVER_SECRET_KEY; required] - --name TEXT The name of the target in the Vuforia database. - --image FILE The path to an image to upload and set as the - target image. - --active-flag [true|false] Whether or not the target is active for query. - --target-id TEXT The ID of a target in the Vuforia database. - [required] - --base-vws-url TEXT The base URL for the VWS API. [default: - https://vws.vuforia.com] - -h, --help Show this message and exit. + --width FLOAT The width of the target in the Vuforia + database. + --application-metadata TEXT The base64 encoded application metadata + associated with the target. + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --name TEXT The name of the target in the Vuforia + database. + --image FILE The path to an image to upload and set as the + target image. + --active-flag [true|false] Whether or not the target is active for query. + --target-id TEXT The ID of a target in the Vuforia database. + [required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___wait_for_model_target_dataset_generated___.txt b/tests/test_help/test_vws_command_help___wait_for_model_target_dataset_generated___.txt new file mode 100644 index 00000000..bdb85832 --- /dev/null +++ b/tests/test_help/test_vws_command_help___wait_for_model_target_dataset_generated___.txt @@ -0,0 +1,42 @@ +Usage: vws wait-for-model-target-dataset-generated [OPTIONS] + + Wait for Vuforia to finish generating a Model Target dataset. + + This is done by polling the Model Target Web API. The status of the dataset is + shown once Vuforia has finished with it. A dataset which failed to generate is + also finished, and gives a non-zero exit code. + +Options: + --seconds-between-requests FLOAT RANGE + The number of seconds to wait between requests + made while polling the dataset status. We wait + 0.2 seconds by default, rather than less, than + that to decrease the number of calls made to + the API, to decrease the likelihood of hitting + the request quota. [default: 0.2; x>=0.05] + --timeout-seconds FLOAT RANGE The maximum number of seconds to wait for the + dataset to be generated. [default: 300; + x>=0.05] + --dataset-uuid TEXT The UUID of a Model Target dataset, as given + when the dataset was created. [required] + --dataset-type [standard|advanced] + The kind of Model Target dataset. Standard and + advanced datasets are separate resources, so a + dataset created as one type is not visible to + requests for the other type. [default: + standard] + --client-id TEXT A Vuforia OAuth2 client ID to use to access + the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_ID; required] + --client-secret TEXT A Vuforia OAuth2 client secret to use to + access the Model Target Web API. [env var: + VUFORIA_MODEL_TARGET_CLIENT_SECRET; required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + -h, --help Show this message and exit. diff --git a/tests/test_help/test_vws_command_help___wait_for_target_processed___.txt b/tests/test_help/test_vws_command_help___wait_for_target_processed___.txt index 7b07cbc0..9e212530 100644 --- a/tests/test_help/test_vws_command_help___wait_for_target_processed___.txt +++ b/tests/test_help/test_vws_command_help___wait_for_target_processed___.txt @@ -23,4 +23,10 @@ Options: [required] --base-vws-url TEXT The base URL for the VWS API. [default: https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] -h, --help Show this message and exit. diff --git a/tests/test_model_target.py b/tests/test_model_target.py new file mode 100644 index 00000000..006a8803 --- /dev/null +++ b/tests/test_model_target.py @@ -0,0 +1,986 @@ +"""Tests for the Model Target dataset commands.""" + +import json +import uuid +import zipfile +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +import pytest +from click.testing import CliRunner +from mock_vws import ( + MockVWS, + ModelTargetGenerationFailure, + ModelTargetGenerationWarning, +) +from vws.exceptions.custom_exceptions import ServerError +from vws.exceptions.model_target_exceptions import ( + ModelTargetAuthenticationError, + ModelTargetError, + ModelTargetValidationError, +) +from vws.response import Response + +from vws_cli import vws_group +from vws_cli._error_handling import get_model_target_error_message + +# The credentials which ``vws-python-mock`` accepts for the Model Target +# Web API. +_CLIENT_ID = "client-id" +_CLIENT_SECRET = "client-secret" # noqa: S105 + +_CREDENTIAL_ARGS = [ + "--client-id", + _CLIENT_ID, + "--client-secret", + _CLIENT_SECRET, +] + +_CAD_DATA_URL = "https://example.com/model.zip" + +# The exit code which ``click`` uses for a usage error. +_USAGE_ERROR_EXIT_CODE = 2 + + +@pytest.fixture(name="model_target_mock") +def fixture_model_target_mock() -> Iterator[MockVWS]: + """Yield a mock which generates Model Target datasets immediately.""" + with MockVWS(processing_time_seconds=0) as mock: + yield mock + + +def _create_dataset( + *, + runner: CliRunner, + extra_args: list[str], +) -> str: + """Create a dataset from one model, and return the dataset UUID.""" + args = [ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--model-name", + "my-model", + "--cad-data-url", + _CAD_DATA_URL, + *_CREDENTIAL_ARGS, + *extra_args, + ] + result = runner.invoke( + cli=vws_group, + args=args, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0, result.output + return result.stdout.strip() + + +def _response(*, status_code: int, text: str) -> Response: + """Return a response, as ``VWS-Python`` gives to an exception.""" + return Response( + text=text, + url="https://vws.vuforia.com/modeltargets/datasets", + status_code=status_code, + headers={}, + request_body=None, + tell_position=0, + content=text.encode(encoding="utf-8"), + ) + + +@pytest.mark.parametrize( + argnames="dataset_type", argvalues=["standard", "advanced"] +) +@pytest.mark.usefixtures("model_target_mock") +def test_dataset_lifecycle(*, dataset_type: str, tmp_path: Path) -> None: + """A dataset can be created, polled, downloaded and deleted.""" + runner = CliRunner() + type_args = ["--dataset-type", dataset_type] + dataset_uuid = _create_dataset(runner=runner, extra_args=type_args) + dataset_args = [ + "--dataset-uuid", + dataset_uuid, + *type_args, + *_CREDENTIAL_ARGS, + ] + + status_result = runner.invoke( + cli=vws_group, + args=["get-model-target-dataset-status", *dataset_args], + catch_exceptions=False, + color=True, + ) + assert status_result.exit_code == 0 + assert f"dataset_uuid: {dataset_uuid}" in status_result.stdout + assert "status: done" in status_result.stdout + + wait_result = runner.invoke( + cli=vws_group, + args=["wait-for-model-target-dataset-generated", *dataset_args], + catch_exceptions=False, + color=True, + ) + assert wait_result.exit_code == 0 + assert not wait_result.stderr + assert "status: done" in wait_result.stdout + + output_file_path = tmp_path / "dataset.zip" + download_result = runner.invoke( + cli=vws_group, + args=[ + "download-model-target-dataset", + "--output", + str(object=output_file_path), + *dataset_args, + ], + catch_exceptions=False, + color=True, + ) + assert download_result.exit_code == 0 + assert not download_result.stdout + assert zipfile.is_zipfile(filename=output_file_path) + + delete_result = runner.invoke( + cli=vws_group, + args=["delete-model-target-dataset", *dataset_args], + catch_exceptions=False, + color=True, + ) + assert delete_result.exit_code == 0 + assert not delete_result.stdout + + deleted_status_result = runner.invoke( + cli=vws_group, + args=["get-model-target-dataset-status", *dataset_args], + catch_exceptions=False, + color=True, + ) + assert deleted_status_result.exit_code == 1 + assert deleted_status_result.stderr == ( + "Error: No Model Target dataset of the given type matches the given " + "UUID.\n" + ) + + +@pytest.mark.usefixtures("model_target_mock") +def test_dataset_types_are_separate() -> None: + """A standard dataset is not visible to advanced dataset requests.""" + runner = CliRunner() + dataset_uuid = _create_dataset(runner=runner, extra_args=[]) + result = runner.invoke( + cli=vws_group, + args=[ + "get-model-target-dataset-status", + "--dataset-uuid", + dataset_uuid, + "--dataset-type", + "advanced", + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + assert result.stderr == ( + "Error: No Model Target dataset of the given type matches the given " + "UUID.\n" + ) + + +@pytest.mark.usefixtures("model_target_mock") +def test_cad_data_file(*, tmp_path: Path) -> None: + """A model's CAD data can be given as a file.""" + runner = CliRunner() + cad_data_file_path = tmp_path / "model.obj" + cad_data_file_path.write_bytes(data=b"\x00cad-data") + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--model-name", + "my-model", + "--cad-data-file", + str(object=cad_data_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + assert result.stdout.strip() + + +@pytest.mark.usefixtures("model_target_mock") +def test_model_options(*, tmp_path: Path) -> None: + """The optional model settings are sent to Vuforia.""" + runner = CliRunner() + state_based_configuration_file_path = tmp_path / "states.json" + state_based_configuration_file_path.write_text( + data=json.dumps(obj={"states": {"open": {}}}), + ) + dataset_uuid = _create_dataset( + runner=runner, + extra_args=[ + "--dataset-type", + "advanced", + "--automatic-coloring", + "always", + "--cad-data-format", + "OBJ", + "--motion-hint", + "static", + "--optimize-tracking-for", + "default", + "--realistic-appearance", + "auto", + "--simplify", + "never", + "--tracking-mode", + "car", + "--state-based-configuration-file", + str(object=state_based_configuration_file_path), + ], + ) + assert dataset_uuid + + +def _models_file(*, tmp_path: Path, models_json: object) -> Path: + """Write a models file, and return its path.""" + models_file_path = tmp_path / "models.json" + models_file_path.write_text(data=json.dumps(obj=models_json)) + return models_file_path + + +@pytest.mark.usefixtures("model_target_mock") +def test_models_file(*, tmp_path: Path) -> None: + """Multiple models, with guide views, can be given in a file.""" + runner = CliRunner() + models_file_path = _models_file( + tmp_path=tmp_path, + models_json=[ + { + "name": "first-model", + "cadDataUrl": _CAD_DATA_URL, + "automaticColoring": "auto", + "cadDataFormat": "OBJ", + "motionHint": "static", + "optimizeTrackingFor": "default", + "realisticAppearance": "true", + "simplify": "auto", + "trackingMode": "default", + "stateBasedConfigurationJsonString": ( + '{"states": {"open": {}}}' + ), + "views": [ + { + "name": "front", + "guideViewPosition": { + "rotation": [0, 0, 0, 1], + "translation": [0.0, 0.0, -1.5], + }, + "states": ["open"], + }, + ], + }, + { + "name": "second-model", + "cadDataBlob": "Y2FkLWRhdGE=", + }, + ], + ) + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--dataset-type", + "advanced", + "--models-file", + str(object=models_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + assert result.stdout.strip() + + +@pytest.mark.usefixtures("model_target_mock") +def test_models_file_with_models_key(*, tmp_path: Path) -> None: + """A models file may hold an object with a ``models`` array.""" + runner = CliRunner() + models_file_path = _models_file( + tmp_path=tmp_path, + models_json={ + "models": [{"name": "my-model", "cadDataUrl": _CAD_DATA_URL}], + }, + ) + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--models-file", + str(object=models_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + assert result.stdout.strip() + + +@pytest.mark.usefixtures("model_target_mock") +def test_models_file_with_model_options(*, tmp_path: Path) -> None: + """``--models-file`` cannot be used with the single-model options.""" + runner = CliRunner() + models_file_path = _models_file( + tmp_path=tmp_path, + models_json=[{"name": "my-model", "cadDataUrl": _CAD_DATA_URL}], + ) + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--model-name", + "my-model", + "--simplify", + "never", + "--models-file", + str(object=models_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == _USAGE_ERROR_EXIT_CODE + assert ( + "Error: --models-file cannot be used with --model-name, --simplify." + in result.stderr + ) + + +@pytest.mark.usefixtures("model_target_mock") +def test_model_name_required() -> None: + """A model name is required when no models file is given.""" + runner = CliRunner() + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--cad-data-url", + _CAD_DATA_URL, + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == _USAGE_ERROR_EXIT_CODE + assert ( + "Error: --model-name is required when --models-file is not." + in result.stderr + ) + + +@pytest.mark.parametrize( + argnames="cad_data_args", + argvalues=[ + pytest.param([], id="neither"), + pytest.param( + ["--cad-data-url", _CAD_DATA_URL, "--cad-data-file", __file__], + id="both", + ), + ], +) +@pytest.mark.usefixtures("model_target_mock") +def test_one_cad_data_source_required(*, cad_data_args: list[str]) -> None: + """Exactly one CAD data source is required for a single model.""" + runner = CliRunner() + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--model-name", + "my-model", + *cad_data_args, + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == _USAGE_ERROR_EXIT_CODE + assert ( + "Error: Exactly one of --cad-data-url and --cad-data-file is required." + in result.stderr + ) + + +_VALID_POSITION: dict[str, Any] = { + "rotation": [0, 0, 0, 1], + "translation": [0, 0, -1], +} + +_VALID_VIEW: dict[str, Any] = { + "name": "front", + "guideViewPosition": _VALID_POSITION, +} + +_EMPTY_OBJECT: dict[str, Any] = {} + +_EMPTY_ARRAY: list[Any] = [] + + +@pytest.mark.parametrize( + argnames=("models_json", "expected_message"), + argvalues=[ + pytest.param( + {"models": "my-model"}, + "/models must be an array.", + id="models-not-an-array", + ), + pytest.param( + ["my-model"], + "/models(0) must be an object.", + id="model-not-an-object", + ), + pytest.param( + [{"cadDataUrl": _CAD_DATA_URL}], + "/models(0)/name is required.", + id="model-without-name", + ), + pytest.param( + [{"name": "my-model", "cadData": _CAD_DATA_URL}], + "/models(0) has unknown fields: cadData.", + id="unknown-model-field", + ), + pytest.param( + [{"name": 1}], + "/models(0)/name must be a string.", + id="model-name-not-a-string", + ), + pytest.param( + [{"name": "my-model", "simplify": "sometimes"}], + "/models(0)/simplify must be one of: always, auto, never.", + id="invalid-enum-value", + ), + pytest.param( + [{"name": "my-model", "views": _EMPTY_OBJECT}], + "/models(0)/views must be an array.", + id="views-not-an-array", + ), + pytest.param( + [{"name": "my-model", "views": ["front"]}], + "/models(0)/views(0) must be an object.", + id="view-not-an-object", + ), + pytest.param( + [{"name": "my-model", "views": [{**_VALID_VIEW, "extra": 1}]}], + "/models(0)/views(0) has unknown fields: extra.", + id="unknown-view-field", + ), + pytest.param( + [{"name": "my-model", "views": [{"name": "front"}]}], + "/models(0)/views(0)/guideViewPosition is required.", + id="view-without-position", + ), + pytest.param( + [ + { + "name": "my-model", + "views": [ + {**_VALID_VIEW, "guideViewPosition": _EMPTY_ARRAY}, + ], + }, + ], + "/models(0)/views(0)/guideViewPosition must be an object.", + id="position-not-an-object", + ), + pytest.param( + [ + { + "name": "my-model", + "views": [ + { + **_VALID_VIEW, + "guideViewPosition": { + "rotation": [0, 0, 0, 1], + }, + }, + ], + }, + ], + "/models(0)/views(0)/guideViewPosition/translation is required.", + id="position-without-translation", + ), + pytest.param( + [ + { + "name": "my-model", + "views": [ + { + **_VALID_VIEW, + "guideViewPosition": { + "rotation": [0, 0, 0, 1], + "translation": [0, 0, -1], + "scale": 1, + }, + }, + ], + }, + ], + "/models(0)/views(0)/guideViewPosition has unknown fields: scale.", + id="unknown-position-field", + ), + pytest.param( + [ + { + "name": "my-model", + "views": [ + { + **_VALID_VIEW, + "guideViewPosition": { + "rotation": "0 0 0 1", + "translation": [0, 0, -1], + }, + }, + ], + }, + ], + ( + "/models(0)/views(0)/guideViewPosition/rotation must be an " + "array of numbers." + ), + id="rotation-not-an-array", + ), + pytest.param( + [ + { + "name": "my-model", + "views": [ + { + **_VALID_VIEW, + "guideViewPosition": { + "rotation": [0, 0, 0, True], + "translation": [0, 0, -1], + }, + }, + ], + }, + ], + ( + "/models(0)/views(0)/guideViewPosition/rotation must be an " + "array of numbers." + ), + id="rotation-with-a-boolean", + ), + pytest.param( + [ + { + "name": "my-model", + "views": [{**_VALID_VIEW, "states": _EMPTY_OBJECT}], + }, + ], + "/models(0)/views(0)/states must be an array of strings.", + id="states-not-an-array", + ), + pytest.param( + [{"name": "my-model", "views": [{**_VALID_VIEW, "states": [1]}]}], + "/models(0)/views(0)/states(0) must be a string.", + id="state-not-a-string", + ), + ], +) +@pytest.mark.usefixtures("model_target_mock") +def test_invalid_models_file( + *, + models_json: object, + expected_message: str, + tmp_path: Path, +) -> None: + """An error is shown for a models file which cannot be used.""" + runner = CliRunner() + models_file_path = _models_file( + tmp_path=tmp_path, + models_json=models_json, + ) + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--models-file", + str(object=models_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == _USAGE_ERROR_EXIT_CODE + expected_error = ( + f"Error: Invalid value for '--models-file': {expected_message}" + ) + assert expected_error in result.stderr + + +@pytest.mark.usefixtures("model_target_mock") +def test_models_file_without_models_key(*, tmp_path: Path) -> None: + """An object models file must have a ``models`` array.""" + runner = CliRunner() + models_file_path = _models_file( + tmp_path=tmp_path, + models_json={"name": "my-dataset"}, + ) + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--models-file", + str(object=models_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == _USAGE_ERROR_EXIT_CODE + assert ( + "Error: Invalid value for '--models-file': /models is required." + in result.stderr + ) + + +@pytest.mark.usefixtures("model_target_mock") +def test_models_file_is_not_json(*, tmp_path: Path) -> None: + """An error is shown for a models file which is not JSON.""" + runner = CliRunner() + models_file_path = tmp_path / "models.json" + models_file_path.write_text(data="not-json") + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--models-file", + str(object=models_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == _USAGE_ERROR_EXIT_CODE + expected_error = ( + f"Error: Invalid value for '--models-file': {models_file_path} is " + "not valid JSON." + ) + assert expected_error in result.stderr + + +@pytest.mark.usefixtures("model_target_mock") +def test_vuforia_validation_error(*, tmp_path: Path) -> None: + """The reasons which Vuforia gives for rejecting a request are + shown. + """ + runner = CliRunner() + models_file_path = _models_file( + tmp_path=tmp_path, + models_json=[ + {"name": "first-model", "cadDataUrl": _CAD_DATA_URL}, + {"name": "second-model", "cadDataUrl": _CAD_DATA_URL}, + ], + ) + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--models-file", + str(object=models_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + assert result.stderr == ( + "Error: Vuforia rejected the request.\n" + "VALIDATION_ERROR: exactly one model should be provided\n" + ) + + +@pytest.mark.usefixtures("model_target_mock") +def test_authentication_failure() -> None: + """An error is shown when the given credentials are not accepted.""" + runner = CliRunner() + result = runner.invoke( + cli=vws_group, + args=[ + "create-model-target-dataset", + "--name", + "my-dataset", + "--target-sdk", + "10.29", + "--model-name", + "my-model", + "--cad-data-url", + _CAD_DATA_URL, + "--client-id", + _CLIENT_ID, + "--client-secret", + "wrong-client-secret", + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + assert result.stderr == ( + "Error: The given client ID and client secret are not a set of Model " + "Target Web API credentials.\n" + ) + + +def test_status_while_processing() -> None: + """The status of a dataset which is being generated is shown.""" + runner = CliRunner() + with MockVWS(processing_time_seconds=9999): + dataset_uuid = _create_dataset(runner=runner, extra_args=[]) + result = runner.invoke( + cli=vws_group, + args=[ + "get-model-target-dataset-status", + "--dataset-uuid", + dataset_uuid, + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 0 + assert "status: processing" in result.stdout + assert "completed_at: null" in result.stdout + + +def test_download_before_generated(*, tmp_path: Path) -> None: + """An error is shown when a dataset is not ready to download.""" + runner = CliRunner() + output_file_path = tmp_path / "dataset.zip" + with MockVWS(processing_time_seconds=9999): + dataset_uuid = _create_dataset(runner=runner, extra_args=[]) + result = runner.invoke( + cli=vws_group, + args=[ + "download-model-target-dataset", + "--dataset-uuid", + dataset_uuid, + "--output", + str(object=output_file_path), + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 1 + assert result.stderr == ( + "Error: Vuforia has not finished generating the dataset, so the " + "dataset cannot be downloaded.\n" + ) + assert not output_file_path.exists() + + +def test_wait_timeout() -> None: + """An error is shown when waiting for a dataset times out.""" + runner = CliRunner() + with MockVWS(processing_time_seconds=9999): + dataset_uuid = _create_dataset(runner=runner, extra_args=[]) + result = runner.invoke( + cli=vws_group, + args=[ + "wait-for-model-target-dataset-generated", + "--dataset-uuid", + dataset_uuid, + "--seconds-between-requests", + "0.05", + "--timeout-seconds", + "0.05", + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 1 + assert result.stderr == "Timeout of 0.05 seconds reached.\n" + + +def test_wait_for_failed_dataset() -> None: + """A dataset which failed to generate gives a non-zero exit code.""" + runner = CliRunner() + failure = ModelTargetGenerationFailure(message="Generation failed") + with MockVWS( + processing_time_seconds=0, + model_target_generation_failure=failure, + ): + dataset_uuid = _create_dataset(runner=runner, extra_args=[]) + result = runner.invoke( + cli=vws_group, + args=[ + "wait-for-model-target-dataset-generated", + "--dataset-uuid", + dataset_uuid, + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 1 + assert "status: failed" in result.stdout + assert "message: Generation failed" in result.stdout + assert result.stderr == "Error: Vuforia failed to generate the dataset.\n" + + +def test_wait_for_dataset_with_warning() -> None: + """A warning about a generated dataset is shown.""" + runner = CliRunner() + warning = ModelTargetGenerationWarning() + with MockVWS( + processing_time_seconds=0, + model_target_generation_warning=warning, + ): + dataset_uuid = _create_dataset(runner=runner, extra_args=[]) + result = runner.invoke( + cli=vws_group, + args=[ + "wait-for-model-target-dataset-generated", + "--dataset-uuid", + dataset_uuid, + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 0 + assert not result.stderr + assert "status: done" in result.stdout + assert f"message: {warning.message}" in result.stdout + + +def test_authentication_error_message() -> None: + """A rejected authenticated request gives a useful message.""" + exc = ModelTargetAuthenticationError( + response=_response(status_code=401, text="Unauthorized"), + ) + assert ( + get_model_target_error_message(exc=exc) + == "Error: The request to Vuforia was not authenticated." + ) + + +def test_validation_error_message_without_details() -> None: + """A validation error without details gives the Vuforia message.""" + body = json.dumps( + obj={"error": {"code": "VALIDATION_ERROR", "message": "Bad request"}}, + ) + exc = ModelTargetValidationError( + response=_response(status_code=400, text=body), + ) + assert get_model_target_error_message(exc=exc) == ( + "Error: Vuforia rejected the request.\nBad request" + ) + + +@pytest.mark.parametrize( + argnames=("text", "expected_message"), + argvalues=[ + pytest.param( + json.dumps(obj={"error": {"code": "X", "message": "Bad request"}}), + "Error: Bad request", + id="with-message", + ), + pytest.param( + "Bad Request", + "Error: Vuforia returned an error.", + id="without-message", + ), + ], +) +def test_other_error_message(*, text: str, expected_message: str) -> None: + """Another error from Vuforia gives a useful message.""" + exc = ModelTargetError(response=_response(status_code=403, text=text)) + assert get_model_target_error_message(exc=exc) == expected_message + + +def test_server_error_message() -> None: + """An error from the Vuforia servers gives a useful message.""" + exc = ServerError( + response=_response(status_code=500, text="Internal Server Error"), + ) + assert get_model_target_error_message(exc=exc) == ( + "Error: There was an unknown error from Vuforia. This may be because " + "there is a problem with the given name." + ) + + +def test_unknown_dataset_uuid() -> None: + """An error is shown for a UUID which does not match a dataset.""" + runner = CliRunner() + with MockVWS(): + result = runner.invoke( + cli=vws_group, + args=[ + "delete-model-target-dataset", + "--dataset-uuid", + uuid.uuid4().hex, + *_CREDENTIAL_ARGS, + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 1 + assert result.stderr == ( + "Error: No Model Target dataset of the given type matches the given " + "UUID.\n" + ) diff --git a/tests/test_query.py b/tests/test_query.py index 9b09e559..5fe1ea1d 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,35 +1,34 @@ -""" -Test for the Cloud Reco Service commands. -""" +"""Test for the Cloud Reco Service commands.""" +import datetime import io import uuid from pathlib import Path from textwrap import dedent +import pytest +import requests import yaml from click.testing import CliRunner +from freezegun import freeze_time from mock_vws import MockVWS -from mock_vws.database import VuforiaDatabase +from mock_vws.database import CloudDatabase from vws import VWS from vws_cli.query import vuforia_cloud_reco class TestQuery: - """ - Tests for making image queries. - """ + """Tests for making image queries.""" @staticmethod def test_no_matches( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, tmp_path: Path, high_quality_image: io.BytesIO, ) -> None: - """ - An empty list is returned if there are no matches. - """ + """An empty list is returned if there are no matches.""" runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() @@ -53,14 +52,13 @@ def test_no_matches( @staticmethod def test_matches( + *, tmp_path: Path, high_quality_image: io.BytesIO, vws_client: VWS, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: - """ - Details of matching targets are shown. - """ + """Details of matching targets are shown.""" name = uuid.uuid4().hex target_id = vws_client.add_target( name=name, @@ -104,11 +102,13 @@ def test_matches( @staticmethod def test_image_file_is_dir( + *, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: """ - An appropriate error is given if the given image file path points to a + An appropriate error is given if the given image file path + points to a directory. """ runner = CliRunner() @@ -140,13 +140,12 @@ def test_image_file_is_dir( @staticmethod def test_relative_path( + *, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, high_quality_image: io.BytesIO, ) -> None: - """ - Image file paths are resolved. - """ + """Image file paths are resolved.""" runner = CliRunner() new_filename = uuid.uuid4().hex original_image_file = tmp_path / "foo" @@ -175,11 +174,13 @@ def test_relative_path( @staticmethod def test_image_file_does_not_exist( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, tmp_path: Path, ) -> None: """ - An appropriate error is given if the given image file does not exist. + An appropriate error is given if the given image file does not + exist. """ runner = CliRunner() does_not_exist_file = tmp_path / uuid.uuid4().hex @@ -210,10 +211,164 @@ def test_image_file_does_not_exist( assert result.stderr == expected_stderr +class TestDefaultRequestTimeout: + """Tests for the default request timeout.""" + + @staticmethod + @pytest.mark.parametrize( + argnames=("response_delay_seconds", "expect_timeout"), + argvalues=[(29, False), (31, True)], + ) + def test_default_timeout( + *, + high_quality_image: io.BytesIO, + tmp_path: Path, + response_delay_seconds: int, + expect_timeout: bool, + ) -> None: + """At 29 seconds there is no error; at 31 seconds there is a + timeout. + """ + runner = CliRunner() + new_file = tmp_path / uuid.uuid4().hex + image_data = high_quality_image.getvalue() + new_file.write_bytes(data=image_data) + with ( + freeze_time() as frozen_datetime, + MockVWS( + response_delay_seconds=response_delay_seconds, + sleep_fn=lambda seconds: ( + frozen_datetime.tick( + delta=datetime.timedelta(seconds=seconds), + ), + None, + )[1], + ) as mock, + ): + database = CloudDatabase() + mock.add_cloud_database(cloud_database=database) + commands = [ + str(object=new_file), + "--client-access-key", + database.client_access_key, + "--client-secret-key", + database.client_secret_key, + ] + + if expect_timeout: + with pytest.raises( + expected_exception=requests.exceptions.Timeout, + ): + runner.invoke( + cli=vuforia_cloud_reco, + args=commands, + catch_exceptions=False, + color=True, + ) + else: + result = runner.invoke( + cli=vuforia_cloud_reco, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + + +class TestCustomRequestTimeout: + """Tests for custom request timeout options.""" + + @staticmethod + def test_custom_timeout( + *, + high_quality_image: io.BytesIO, + tmp_path: Path, + ) -> None: + """Custom connection and read timeouts are respected.""" + runner = CliRunner() + new_file = tmp_path / uuid.uuid4().hex + image_data = high_quality_image.getvalue() + new_file.write_bytes(data=image_data) + with ( + freeze_time() as frozen_datetime, + MockVWS( + response_delay_seconds=5, + sleep_fn=lambda seconds: ( + frozen_datetime.tick( + delta=datetime.timedelta(seconds=seconds), + ), + None, + )[1], + ) as mock, + ): + database = CloudDatabase() + mock.add_cloud_database(cloud_database=database) + commands = [ + str(object=new_file), + "--client-access-key", + database.client_access_key, + "--client-secret-key", + database.client_secret_key, + "--read-timeout-seconds", + "1", + ] + + with pytest.raises( + expected_exception=requests.exceptions.Timeout, + ): + runner.invoke( + cli=vuforia_cloud_reco, + args=commands, + catch_exceptions=False, + color=True, + ) + + @staticmethod + def test_custom_timeout_no_error( + *, + high_quality_image: io.BytesIO, + tmp_path: Path, + ) -> None: + """A sufficiently large timeout does not cause an error.""" + runner = CliRunner() + new_file = tmp_path / uuid.uuid4().hex + image_data = high_quality_image.getvalue() + new_file.write_bytes(data=image_data) + with ( + freeze_time() as frozen_datetime, + MockVWS( + response_delay_seconds=5, + sleep_fn=lambda seconds: ( + frozen_datetime.tick( + delta=datetime.timedelta(seconds=seconds), + ), + None, + )[1], + ) as mock, + ): + database = CloudDatabase() + mock.add_cloud_database(cloud_database=database) + commands = [ + str(object=new_file), + "--client-access-key", + database.client_access_key, + "--client-secret-key", + database.client_secret_key, + "--read-timeout-seconds", + "60", + ] + + result = runner.invoke( + cli=vuforia_cloud_reco, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + + def test_version() -> None: - """ - ``vuforia-cloud-reco --version`` shows the version. - """ + """``vuforia-cloud-reco --version`` shows the version.""" runner = CliRunner() commands = ["--version"] result = runner.invoke( @@ -227,20 +382,17 @@ def test_version() -> None: class TestMaxNumResults: - """ - Tests for the ``--max-num-results`` option. - """ + """Tests for the ``--max-num-results`` option.""" @staticmethod def test_default( + *, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: - """ - By default the maximum number of results is 1. - """ + """By default the maximum number of results is 1.""" runner = CliRunner() target_id = vws_client.add_target( name=uuid.uuid4().hex, @@ -281,14 +433,13 @@ def test_default( @staticmethod def test_custom( + *, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: - """ - It is possible to set a custom ``--max-num-results``. - """ + """It is possible to set a custom ``--max-num-results``.""" runner = CliRunner() target_id = vws_client.add_target( name=uuid.uuid4().hex, @@ -340,9 +491,10 @@ def test_custom( @staticmethod def test_out_of_range( + *, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: """``--max-num-results`` must be between 1 and 50.""" runner = CliRunner() @@ -374,20 +526,17 @@ def test_out_of_range( class TestIncludeTargetData: - """ - Tests for the ``--include-target-data`` option. - """ + """Tests for the ``--include-target-data`` option.""" @staticmethod def test_default( + *, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: - """ - By default, target data is only returned in the top match. - """ + """By default, target data is only returned in the top match.""" runner = CliRunner() target_id = vws_client.add_target( name=uuid.uuid4().hex, @@ -431,13 +580,15 @@ def test_default( @staticmethod def test_top( + *, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: """ - When 'top' is given, target data is only returned in the top match. + When 'top' is given, target data is only returned in the top + match. """ runner = CliRunner() target_id = vws_client.add_target( @@ -484,13 +635,15 @@ def test_top( @staticmethod def test_none( + *, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: """ - When 'none' is given, target data is not returned in any match. + When 'none' is given, target data is not returned in any + match. """ runner = CliRunner() target_id = vws_client.add_target( @@ -537,14 +690,13 @@ def test_none( @staticmethod def test_all( + *, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: - """ - When 'all' is given, target data is returned in all matches. - """ + """When 'all' is given, target data is returned in all matches.""" runner = CliRunner() target_id = vws_client.add_target( name=uuid.uuid4().hex, @@ -591,12 +743,14 @@ def test_all( @staticmethod def test_other( + *, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: """ - When a string other than 'top', 'all', or 'none' is given, an error is + When a string other than 'top', 'all', or 'none' is given, an + error is shown. """ runner = CliRunner() @@ -629,9 +783,12 @@ def test_other( assert expected_stderr in result.stderr -def test_base_vwq_url(high_quality_image: io.BytesIO, tmp_path: Path) -> None: +def test_base_vwq_url( + *, high_quality_image: io.BytesIO, tmp_path: Path +) -> None: """ - It is possible to use query a target to a database under a custom VWQ URL. + It is possible to use query a target to a database under a custom + VWQ URL. """ runner = CliRunner() base_vwq_url = "http://example.com" @@ -639,8 +796,8 @@ def test_base_vwq_url(high_quality_image: io.BytesIO, tmp_path: Path) -> None: image_data = high_quality_image.getvalue() new_file.write_bytes(data=image_data) with MockVWS(base_vwq_url=base_vwq_url) as mock: - mock_database = VuforiaDatabase() - mock.add_database(database=mock_database) + mock_database = CloudDatabase() + mock.add_cloud_database(cloud_database=mock_database) vws_client = VWS( server_access_key=mock_database.server_access_key, server_secret_key=mock_database.server_secret_key, @@ -678,12 +835,14 @@ def test_base_vwq_url(high_quality_image: io.BytesIO, tmp_path: Path) -> None: def test_env_var_credentials( + *, high_quality_image: io.BytesIO, tmp_path: Path, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: """ - It is possible to use environment variables to set the credentials. + It is possible to use environment variables to set the + credentials. """ runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex diff --git a/tests/test_query_errors.py b/tests/test_query_errors.py index 1f9b7dde..aa8df5f2 100644 --- a/tests/test_query_errors.py +++ b/tests/test_query_errors.py @@ -1,6 +1,4 @@ -""" -Tests for how errors from the Cloud Reco Service are handled by the CLI. -""" +"""Tests for how errors from the Cloud Reco Service are handled by the CLI.""" import io import uuid @@ -9,20 +7,19 @@ from click.testing import CliRunner from freezegun import freeze_time from mock_vws import MockVWS -from mock_vws.database import VuforiaDatabase +from mock_vws.database import CloudDatabase from mock_vws.states import States from vws_cli.query import vuforia_cloud_reco def test_authentication_failure( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, tmp_path: Path, high_quality_image: io.BytesIO, ) -> None: - """ - An error is given when the secret key is incorrect. - """ + """An error is given when the secret key is incorrect.""" runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() @@ -46,13 +43,12 @@ def test_authentication_failure( def test_image_too_large( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, tmp_path: Path, png_too_large: io.BytesIO, ) -> None: - """ - An error is given when the image is too large. - """ + """An error is given when the image is too large.""" runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = png_too_large.getvalue() @@ -76,7 +72,8 @@ def test_image_too_large( def test_bad_image( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, tmp_path: Path, ) -> None: """An error is given when Vuforia returns a ``BadImage`` error. @@ -108,19 +105,21 @@ def test_bad_image( def test_inactive_project( + *, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: """ - An error is given if the project is inactive and the desired action cannot + An error is given if the project is inactive and the desired action + cannot be taken because of this. """ new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() new_file.write_bytes(data=image_data) - database = VuforiaDatabase(state=States.PROJECT_INACTIVE) + database = CloudDatabase(state=States.PROJECT_INACTIVE) with MockVWS() as mock: - mock.add_database(database=database) + mock.add_cloud_database(cloud_database=database) runner = CliRunner() commands = [ str(object=new_file), @@ -145,12 +144,14 @@ def test_inactive_project( def test_request_time_too_skewed( + *, high_quality_image: io.BytesIO, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, tmp_path: Path, ) -> None: """ - An error is given when the request time is more than 65 minutes different + An error is given when the request time is more than 65 minutes + different from the server time. """ runner = CliRunner() diff --git a/tests/test_reco_counts_report.py b/tests/test_reco_counts_report.py new file mode 100644 index 00000000..e9411d15 --- /dev/null +++ b/tests/test_reco_counts_report.py @@ -0,0 +1,300 @@ +"""Tests for the ``get-database-reco-counts-report`` command.""" + +import datetime +import uuid +from pathlib import Path +from zoneinfo import ZoneInfo + +import pytest +from click.testing import CliRunner +from mock_vws import MockVWS +from mock_vws.database import CloudDatabase +from vws import VWS +from vws.exceptions.custom_exceptions import RecoCountsReportNotReadyError + +from vws_cli import vws_group + +_EXPECTED_CSV = b"target_id,reco_count\r\n" + +# The exit code which ``click`` uses for a usage error. +_USAGE_ERROR_EXIT_CODE = 2 + + +def _month_string(*, months_ago: int) -> str: + """Return a ``YYYY-mm`` string for a month relative to this month.""" + now = datetime.datetime.now(tz=ZoneInfo(key="UTC")) + first_of_month = now.replace(day=1) + for _ in range(months_ago): + first_of_month = first_of_month.replace(day=1) - datetime.timedelta( + days=1, + ) + return first_of_month.strftime(format="%Y-%m") + + +def _base_commands(*, mock_database: CloudDatabase) -> list[str]: + """Return the command and credential arguments for the report + command. + """ + return [ + "get-database-reco-counts-report", + "--server-access-key", + mock_database.server_access_key, + "--server-secret-key", + mock_database.server_secret_key, + "--database-id", + mock_database.database_id, + ] + + +def test_get_database_reco_counts_report( + *, + mock_database: CloudDatabase, +) -> None: + """The report is written to stdout. + + The ``mock_database`` fixture does not make the report available + immediately, so this also shows that the command waits for the report. + """ + runner = CliRunner() + result = runner.invoke( + cli=vws_group, + args=_base_commands(mock_database=mock_database), + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + assert not result.stderr + assert result.stdout_bytes == _EXPECTED_CSV + + +def test_report_is_not_available_immediately() -> None: + """The command waits for a report which is not ready to download.""" + runner = CliRunner() + mock_database = CloudDatabase() + with MockVWS(processing_time_seconds=1) as mock: + mock.add_cloud_database(cloud_database=mock_database) + vws_client = VWS( + server_access_key=mock_database.server_access_key, + server_secret_key=mock_database.server_secret_key, + ) + no_wait_result = runner.invoke( + cli=vws_group, + args=[*_base_commands(mock_database=mock_database), "--no-wait"], + catch_exceptions=False, + color=True, + ) + assert no_wait_result.exit_code == 0 + presigned_url = no_wait_result.stdout.strip() + with pytest.raises(expected_exception=RecoCountsReportNotReadyError): + vws_client.download_reco_counts_report( + presigned_url=presigned_url, + ) + + result = runner.invoke( + cli=vws_group, + args=_base_commands(mock_database=mock_database), + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 0 + assert result.stdout_bytes == _EXPECTED_CSV + + +def test_output_file(*, tmp_path: Path) -> None: + """The report is written to the path given by ``--output``.""" + runner = CliRunner() + mock_database = CloudDatabase() + output_file_path = tmp_path / uuid.uuid4().hex + with MockVWS(processing_time_seconds=0) as mock: + mock.add_cloud_database(cloud_database=mock_database) + result = runner.invoke( + cli=vws_group, + args=[ + *_base_commands(mock_database=mock_database), + "--output", + str(object=output_file_path), + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 0 + assert not result.stdout + assert output_file_path.read_bytes() == _EXPECTED_CSV + + +def test_no_wait() -> None: + """``--no-wait`` shows the URL to download the report from.""" + runner = CliRunner() + mock_database = CloudDatabase() + with MockVWS(processing_time_seconds=0) as mock: + mock.add_cloud_database(cloud_database=mock_database) + vws_client = VWS( + server_access_key=mock_database.server_access_key, + server_secret_key=mock_database.server_secret_key, + ) + result = runner.invoke( + cli=vws_group, + args=[*_base_commands(mock_database=mock_database), "--no-wait"], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + presigned_url = result.stdout.strip() + report = vws_client.download_reco_counts_report( + presigned_url=presigned_url, + ) + + assert report.raw_csv == _EXPECTED_CSV + + +def test_no_wait_with_output_file( + *, + mock_database: CloudDatabase, + tmp_path: Path, +) -> None: + """``--output`` cannot be used with ``--no-wait``.""" + runner = CliRunner() + output_file_path = tmp_path / uuid.uuid4().hex + result = runner.invoke( + cli=vws_group, + args=[ + *_base_commands(mock_database=mock_database), + "--no-wait", + "--output", + str(object=output_file_path), + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == _USAGE_ERROR_EXIT_CODE + assert "--output cannot be used with --no-wait." in result.stderr + assert not output_file_path.exists() + + +def test_previous_month() -> None: + """A report can be requested for the previous month.""" + runner = CliRunner() + mock_database = CloudDatabase() + with MockVWS(processing_time_seconds=0) as mock: + mock.add_cloud_database(cloud_database=mock_database) + result = runner.invoke( + cli=vws_group, + args=[ + *_base_commands(mock_database=mock_database), + "--month", + _month_string(months_ago=1), + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 0 + assert result.stdout_bytes == _EXPECTED_CSV + + +def test_month_is_not_in_the_yyyy_mm_form( + *, + mock_database: CloudDatabase, +) -> None: + """An error is shown for a month which is not in the ``YYYY-mm`` + form. + """ + runner = CliRunner() + result = runner.invoke( + cli=vws_group, + args=[ + *_base_commands(mock_database=mock_database), + "--month", + "not-a-month", + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == _USAGE_ERROR_EXIT_CODE + expected_message = '"not-a-month" is not a month in the YYYY-mm form.' + assert expected_message in result.stderr + + +def test_month_out_of_range(*, mock_database: CloudDatabase) -> None: + """An error is shown for a month which Vuforia does not accept. + + Vuforia accepts only the current month and the previous month. + """ + runner = CliRunner() + result = runner.invoke( + cli=vws_group, + args=[ + *_base_commands(mock_database=mock_database), + "--month", + _month_string(months_ago=2), + ], + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = ( + "Error: The request made to Vuforia was invalid and could not be " + "processed. Check the given parameters.\n" + ) + assert result.stderr == expected_stderr + assert not result.stdout + + +def test_database_id_does_not_match( + *, + mock_database: CloudDatabase, +) -> None: + """An error is shown when the given database ID is not the + database's. + """ + runner = CliRunner() + commands = [ + "get-database-reco-counts-report", + "--server-access-key", + mock_database.server_access_key, + "--server-secret-key", + mock_database.server_secret_key, + "--database-id", + uuid.uuid4().hex, + ] + result = runner.invoke( + cli=vws_group, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = ( + "Error: The given secret key was incorrect, or the given database ID " + "is not the ID of the database which the given server keys belong " + "to.\n" + ) + assert result.stderr == expected_stderr + assert not result.stdout + + +def test_timeout_reached() -> None: + """An error is shown when the report is not generated in time.""" + runner = CliRunner() + mock_database = CloudDatabase() + timeout_seconds = 0.1 + with MockVWS(processing_time_seconds=60) as mock: + mock.add_cloud_database(cloud_database=mock_database) + result = runner.invoke( + cli=vws_group, + args=[ + *_base_commands(mock_database=mock_database), + "--timeout-seconds", + str(object=timeout_seconds), + "--seconds-between-requests", + "0.05", + ], + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 1 + assert result.stderr == f"Timeout of {timeout_seconds} seconds reached.\n" + assert not result.stdout diff --git a/tests/test_vumark.py b/tests/test_vumark.py new file mode 100644 index 00000000..91136fa5 --- /dev/null +++ b/tests/test_vumark.py @@ -0,0 +1,269 @@ +"""Tests for the ``vumark`` CLI command.""" + +import uuid +from pathlib import Path + +import click +import pytest +from click.testing import CliRunner +from mock_vws import MockVWS +from mock_vws.database import CloudDatabase, VuMarkDatabase +from mock_vws.target import VuMarkTarget + +from vws_cli import vumark as vumark_module + +generate_vumark = vumark_module.generate_vumark + + +class TestGenerateVuMark: + """Tests for ``vumark``.""" + + @staticmethod + @pytest.mark.parametrize( + argnames=("format_name", "expected_prefix"), + argvalues=[ + pytest.param("png", b"\x89PNG\r\n\x1a\n", id="png"), + pytest.param("svg", b"<", id="svg"), + pytest.param("pdf", b"%PDF", id="pdf"), + ], + ) + def test_generate_vumark_format( + vumark_database: VuMarkDatabase, + vumark_target: VuMarkTarget, + tmp_path: Path, + format_name: str, + expected_prefix: bytes, + ) -> None: + """The returned file matches the requested format.""" + runner = CliRunner() + output_file = tmp_path / f"output.{format_name}" + commands = [ + "--target-id", + vumark_target.target_id, + "--instance-id", + "12345", + "--format", + format_name, + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + vumark_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + assert output_file.read_bytes().startswith(expected_prefix) + + @staticmethod + def test_default_format_is_png( + vumark_database: VuMarkDatabase, + vumark_target: VuMarkTarget, + tmp_path: Path, + ) -> None: + """The default output format is png.""" + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + vumark_target.target_id, + "--instance-id", + "12345", + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + vumark_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + assert output_file.exists() + assert output_file.read_bytes().startswith(b"\x89PNG\r\n\x1a\n") + + @staticmethod + def test_unknown_target( + vumark_database: VuMarkDatabase, + tmp_path: Path, + ) -> None: + """An error is shown when the target ID does not exist.""" + nonexistent_target_id = uuid.uuid4().hex + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + nonexistent_target_id, + "--instance-id", + "12345", + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + vumark_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = ( + f'Error: Target "{nonexistent_target_id}" does not exist.\n' + ) + assert result.stderr == expected_stderr + + @staticmethod + def test_invalid_instance_id( + vumark_database: VuMarkDatabase, + vumark_target: VuMarkTarget, + tmp_path: Path, + ) -> None: + """An error is shown when the instance ID is invalid.""" + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + vumark_target.target_id, + "--instance-id", + "", + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + vumark_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = "Error: The given instance ID is invalid.\n" + assert result.stderr == expected_stderr + + @staticmethod + def test_target_not_in_success_state( + tmp_path: Path, + ) -> None: + """An error is shown when the target is not in the success + state. + """ + processing_target = VuMarkTarget( + name="processing-target", + processing_time_seconds=9999, + ) + database = VuMarkDatabase(vumark_targets={processing_target}) + with MockVWS() as mock: + mock.add_vumark_database(vumark_database=database) + target_id = processing_target.target_id + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + target_id, + "--instance-id", + "12345", + "--output", + str(object=output_file), + "--server-access-key", + database.server_access_key, + "--server-secret-key", + database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = ( + f'Error: The target "{target_id}" is not in the success ' + "state and cannot be used to generate a VuMark instance.\n" + ) + assert result.stderr == expected_stderr + + @staticmethod + def test_authentication_failure( + vumark_database: VuMarkDatabase, + vumark_target: VuMarkTarget, + tmp_path: Path, + ) -> None: + """An error is shown on authentication failure.""" + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + vumark_target.target_id, + "--instance-id", + "12345", + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + "wrong-secret-key", + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = "The given secret key was incorrect.\n" + assert result.stderr == expected_stderr + + +@pytest.mark.parametrize(argnames="invalid_format", argvalues=["bmp", "gif"]) +def test_invalid_format( + mock_database: CloudDatabase, + tmp_path: Path, + invalid_format: str, +) -> None: + """An error is shown for an unrecognized format choice.""" + runner = CliRunner() + output_file = tmp_path / "output" + commands = [ + "--target-id", + "some-target-id", + "--instance-id", + "12345", + "--format", + invalid_format, + "--output", + str(object=output_file), + "--server-access-key", + mock_database.server_access_key, + "--server-secret-key", + mock_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == click.UsageError.exit_code + expected_output = ( + "Usage: vumark [OPTIONS]\n" + "Try 'vumark --help' for help.\n" + "\n" + f"Error: Invalid value for '--format': '{invalid_format}' is not " + "one of 'png', 'svg', 'pdf'.\n" + ) + assert result.output == expected_output diff --git a/tests/test_vws_commands.py b/tests/test_vws_commands.py index 9996fd14..2b21a34f 100644 --- a/tests/test_vws_commands.py +++ b/tests/test_vws_commands.py @@ -1,9 +1,8 @@ # pylint:disable=too-many-lines -""" -Tests for VWS CLI commands. -""" +"""Tests for VWS CLI commands.""" import base64 +import datetime import io import secrets import uuid @@ -11,11 +10,12 @@ from textwrap import dedent import pytest +import requests import yaml from click.testing import CliRunner from freezegun import freeze_time from mock_vws import MockVWS -from mock_vws.database import VuforiaDatabase +from mock_vws.database import CloudDatabase from vws import VWS, CloudRecoService from vws.reports import TargetStatuses @@ -23,13 +23,12 @@ def test_get_database_summary_report( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, ) -> None: - """ - It is possible to get a database summary report. - """ + """It is possible to get a database summary report.""" runner = CliRunner() for name in ("a", "b"): vws_client.add_target( @@ -73,13 +72,12 @@ def test_get_database_summary_report( def test_list_targets( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, ) -> None: - """ - It is possible to get a list of targets in the database. - """ + """It is possible to get a list of targets in the database.""" runner = CliRunner() target_id_1 = vws_client.add_target( name="x1", @@ -116,13 +114,12 @@ def test_list_targets( def test_get_target_record( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, ) -> None: - """ - It is possible to get a target record. - """ + """It is possible to get a target record.""" runner = CliRunner() target_id = vws_client.add_target( name="x", @@ -160,13 +157,12 @@ def test_get_target_record( def test_get_target_summary_report( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, ) -> None: - """ - It is possible to get a target summary report. - """ + """It is possible to get a target summary report.""" runner = CliRunner() upload_date = "2015-04-29" with freeze_time(time_to_freeze=upload_date): @@ -210,13 +206,12 @@ def test_get_target_summary_report( def test_delete_target( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, ) -> None: - """ - It is possible to delete a target. - """ + """It is possible to delete a target.""" runner = CliRunner() target_id = vws_client.add_target( name="x", @@ -248,13 +243,12 @@ def test_delete_target( def test_get_duplicate_targets( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, ) -> None: - """ - It is possible to get a list of duplicate targets. - """ + """It is possible to get a list of duplicate targets.""" runner = CliRunner() target_id = vws_client.add_target( name="x", @@ -295,22 +289,193 @@ def test_get_duplicate_targets( assert result_data == expected_result_data +class TestDefaultRequestTimeout: + """Tests for the default request timeout.""" + + @staticmethod + @pytest.mark.parametrize( + argnames=("response_delay_seconds", "expect_timeout"), + argvalues=[(29, False), (31, True)], + ) + def test_default_timeout( + *, + high_quality_image: io.BytesIO, + tmp_path: Path, + response_delay_seconds: int, + expect_timeout: bool, + ) -> None: + """At 29 seconds there is no error; at 31 seconds there is a + timeout. + """ + runner = CliRunner() + new_file = tmp_path / uuid.uuid4().hex + image_data = high_quality_image.getvalue() + new_file.write_bytes(data=image_data) + with ( + freeze_time() as frozen_datetime, + MockVWS( + response_delay_seconds=response_delay_seconds, + sleep_fn=lambda seconds: ( + frozen_datetime.tick( + delta=datetime.timedelta(seconds=seconds), + ), + None, + )[1], + ) as mock, + ): + database = CloudDatabase() + mock.add_cloud_database(cloud_database=database) + commands = [ + "add-target", + "--name", + uuid.uuid4().hex, + "--width", + "1", + "--image", + str(object=new_file), + "--server-access-key", + database.server_access_key, + "--server-secret-key", + database.server_secret_key, + ] + + if expect_timeout: + with pytest.raises( + expected_exception=requests.exceptions.Timeout, + ): + runner.invoke( + cli=vws_group, + args=commands, + catch_exceptions=False, + color=True, + ) + else: + result = runner.invoke( + cli=vws_group, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + + +class TestCustomRequestTimeout: + """Tests for custom request timeout options.""" + + @staticmethod + def test_custom_timeout( + *, + high_quality_image: io.BytesIO, + tmp_path: Path, + ) -> None: + """Custom connection and read timeouts are respected.""" + runner = CliRunner() + new_file = tmp_path / uuid.uuid4().hex + image_data = high_quality_image.getvalue() + new_file.write_bytes(data=image_data) + with ( + freeze_time() as frozen_datetime, + MockVWS( + response_delay_seconds=5, + sleep_fn=lambda seconds: ( + frozen_datetime.tick( + delta=datetime.timedelta(seconds=seconds), + ), + None, + )[1], + ) as mock, + ): + database = CloudDatabase() + mock.add_cloud_database(cloud_database=database) + commands = [ + "add-target", + "--name", + uuid.uuid4().hex, + "--width", + "1", + "--image", + str(object=new_file), + "--server-access-key", + database.server_access_key, + "--server-secret-key", + database.server_secret_key, + "--read-timeout-seconds", + "1", + ] + + with pytest.raises( + expected_exception=requests.exceptions.Timeout, + ): + runner.invoke( + cli=vws_group, + args=commands, + catch_exceptions=False, + color=True, + ) + + @staticmethod + def test_custom_timeout_no_error( + *, + high_quality_image: io.BytesIO, + tmp_path: Path, + ) -> None: + """A sufficiently large timeout does not cause an error.""" + runner = CliRunner() + new_file = tmp_path / uuid.uuid4().hex + image_data = high_quality_image.getvalue() + new_file.write_bytes(data=image_data) + with ( + freeze_time() as frozen_datetime, + MockVWS( + response_delay_seconds=5, + sleep_fn=lambda seconds: ( + frozen_datetime.tick( + delta=datetime.timedelta(seconds=seconds), + ), + None, + )[1], + ) as mock, + ): + database = CloudDatabase() + mock.add_cloud_database(cloud_database=database) + commands = [ + "add-target", + "--name", + uuid.uuid4().hex, + "--width", + "1", + "--image", + str(object=new_file), + "--server-access-key", + database.server_access_key, + "--server-secret-key", + database.server_secret_key, + "--read-timeout-seconds", + "60", + ] + + result = runner.invoke( + cli=vws_group, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + + class TestAddTarget: - """ - Tests for ``vws add-target``. - """ + """Tests for ``vws add-target``.""" @staticmethod def test_add_target( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, cloud_reco_client: CloudRecoService, ) -> None: - """ - It is possible to add a target. - """ + """It is possible to add a target.""" runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex name = uuid.uuid4().hex @@ -354,11 +519,13 @@ def test_add_target( @staticmethod def test_image_file_does_not_exist( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, tmp_path: Path, ) -> None: """ - An appropriate error is given if the given image file does not exist. + An appropriate error is given if the given image file does not + exist. """ runner = CliRunner() does_not_exist_file = tmp_path / uuid.uuid4().hex @@ -387,7 +554,7 @@ def test_image_file_does_not_exist( expected_stderr = dedent( text=f"""\ Usage: vws add-target [OPTIONS] - Try 'vws add-target -h' for help. + Try 'vws add-target --help' for help. Error: Invalid value for '--image': File '{does_not_exist_file}' does not exist. """, @@ -396,11 +563,13 @@ def test_image_file_does_not_exist( @staticmethod def test_image_file_is_dir( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, tmp_path: Path, ) -> None: """ - An appropriate error is given if the given image file path points to a + An appropriate error is given if the given image file path + points to a directory. """ runner = CliRunner() @@ -429,7 +598,7 @@ def test_image_file_is_dir( expected_stderr = dedent( text=f"""\ Usage: vws add-target [OPTIONS] - Try 'vws add-target -h' for help. + Try 'vws add-target --help' for help. Error: Invalid value for '--image': File '{tmp_path}' is a directory. """, @@ -438,14 +607,13 @@ def test_image_file_is_dir( @staticmethod def test_relative_path( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: - """ - Image file paths are resolved. - """ + """Image file paths are resolved.""" runner = CliRunner() new_filename = uuid.uuid4().hex original_image_file = tmp_path / "foo" @@ -481,15 +649,14 @@ def test_relative_path( @staticmethod def test_custom_metadata( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, cloud_reco_client: CloudRecoService, vws_client: VWS, tmp_path: Path, high_quality_image: io.BytesIO, ) -> None: - """ - Custom metadata can be given. - """ + """Custom metadata can be given.""" runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex name = uuid.uuid4().hex @@ -540,17 +707,15 @@ def test_custom_metadata( ], ) def test_custom_active_flag( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, active_flag_given: str, - *, active_flag_expected: bool, ) -> None: - """ - The Active Flag of the new target can be chosen. - """ + """The Active Flag of the new target can be chosen.""" runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() @@ -585,18 +750,18 @@ def test_custom_active_flag( class TestWaitForTargetProcessed: - """ - Tests for ``vws wait-for-target-processed``. - """ + """Tests for ``vws wait-for-target-processed``.""" @staticmethod def test_wait_for_target_processed( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, ) -> None: """ - It is possible to use a command to wait for a target to be processed. + It is possible to use a command to wait for a target to be + processed. """ runner = CliRunner() target_id = vws_client.add_target( @@ -632,13 +797,11 @@ def test_wait_for_target_processed( def test_default_seconds_between_requests( high_quality_image: io.BytesIO, ) -> None: - """ - By default, 0.2 seconds are waited between polling requests. - """ + """By default, 0.2 seconds are waited between polling requests.""" runner = CliRunner() with MockVWS(processing_time_seconds=0.5) as mock: - mock_database = VuforiaDatabase() - mock.add_database(database=mock_database) + mock_database = CloudDatabase() + mock.add_cloud_database(cloud_database=mock_database) vws_client = VWS( server_access_key=mock_database.server_access_key, server_secret_key=mock_database.server_secret_key, @@ -702,12 +865,13 @@ def test_custom_seconds_between_requests( high_quality_image: io.BytesIO, ) -> None: """ - It is possible to customize the time waited between polling requests. + It is possible to customize the time waited between polling + requests. """ runner = CliRunner() with MockVWS(processing_time_seconds=0.5) as mock: - mock_database = VuforiaDatabase() - mock.add_database(database=mock_database) + mock_database = CloudDatabase() + mock.add_cloud_database(cloud_database=mock_database) vws_client = VWS( server_access_key=mock_database.server_access_key, server_secret_key=mock_database.server_secret_key, @@ -766,9 +930,10 @@ def test_custom_seconds_between_requests( assert report.request_usage == expected_requests @staticmethod - def test_custom_seconds_too_small(mock_database: VuforiaDatabase) -> None: + def test_custom_seconds_too_small(mock_database: CloudDatabase) -> None: """ - The minimum valid value for ``--seconds-between-requests`` is 0.05 + The minimum valid value for ``--seconds-between-requests`` is + 0.05 seconds. """ runner = CliRunner() @@ -796,13 +961,11 @@ def test_custom_seconds_too_small(mock_database: VuforiaDatabase) -> None: @staticmethod def test_custom_timeout(high_quality_image: io.BytesIO) -> None: - """ - It is possible to set a maximum timeout. - """ + """It is possible to set a maximum timeout.""" runner = CliRunner() with MockVWS(processing_time_seconds=0.5) as mock: - mock_database = VuforiaDatabase() - mock.add_database(database=mock_database) + mock_database = CloudDatabase() + mock.add_cloud_database(cloud_database=mock_database) vws_client = VWS( server_access_key=mock_database.server_access_key, server_secret_key=mock_database.server_secret_key, @@ -861,9 +1024,10 @@ def test_custom_timeout(high_quality_image: io.BytesIO) -> None: assert report.status != TargetStatuses.PROCESSING @staticmethod - def test_custom_timeout_too_small(mock_database: VuforiaDatabase) -> None: + def test_custom_timeout_too_small(mock_database: CloudDatabase) -> None: """ - The minimum valid value for ``--timeout-seconds`` is 0.05 seconds. + The minimum valid value for ``--timeout-seconds`` is 0.05 + seconds. """ runner = CliRunner() commands = [ @@ -889,23 +1053,19 @@ def test_custom_timeout_too_small(mock_database: VuforiaDatabase) -> None: class TestUpdateTarget: - """ - Tests for ``vws update-target``. - """ + """Tests for ``vws update-target``.""" @staticmethod def test_update_target( *, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, cloud_reco_client: CloudRecoService, different_high_quality_image: io.BytesIO, ) -> None: - """ - It is possible to update a target. - """ + """It is possible to update a target.""" runner = CliRunner() old_name = uuid.uuid4().hex old_width = secrets.choice(seq=range(1, 5000)) / 100 @@ -992,13 +1152,12 @@ def test_update_target( @staticmethod def test_no_fields_given( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, ) -> None: - """ - It is possible to give no update fields. - """ + """It is possible to give no update fields.""" runner = CliRunner() old_name = uuid.uuid4().hex old_width = secrets.choice(seq=range(1, 5000)) / 100 @@ -1031,13 +1190,15 @@ def test_no_fields_given( @staticmethod def test_image_file_does_not_exist( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: """ - An appropriate error is given if the given image file does not exist. + An appropriate error is given if the given image file does not + exist. """ target_id = vws_client.add_target( name="x", @@ -1072,7 +1233,7 @@ def test_image_file_does_not_exist( expected_stderr = dedent( text=f"""\ Usage: vws update-target [OPTIONS] - Try 'vws update-target -h' for help. + Try 'vws update-target --help' for help. Error: Invalid value for '--image': File '{does_not_exist_file}' does not exist. """, @@ -1081,13 +1242,15 @@ def test_image_file_does_not_exist( @staticmethod def test_image_file_is_dir( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: """ - An appropriate error is given if the given image file path points to a + An appropriate error is given if the given image file path + points to a directory. """ target_id = vws_client.add_target( @@ -1122,7 +1285,7 @@ def test_image_file_is_dir( expected_stderr = dedent( text=f"""\ Usage: vws update-target [OPTIONS] - Try 'vws update-target -h' for help. + Try 'vws update-target --help' for help. Error: Invalid value for '--image': File '{tmp_path}' is a directory. """, @@ -1131,14 +1294,13 @@ def test_image_file_is_dir( @staticmethod def test_relative_path( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: - """ - Image file paths are resolved. - """ + """Image file paths are resolved.""" runner = CliRunner() target_id = vws_client.add_target( name="x", @@ -1179,12 +1341,13 @@ def test_relative_path( def test_custom_base_url() -> None: """ - It is possible to use the API to connect to a database under a custom VWS + It is possible to use the API to connect to a database under a + custom VWS URL. """ runner = CliRunner() base_vws_url = "http://example.com" - mock_database = VuforiaDatabase() + mock_database = CloudDatabase() commands = [ "list-targets", "--server-access-key", @@ -1195,7 +1358,7 @@ def test_custom_base_url() -> None: base_vws_url, ] with MockVWS(base_vws_url=base_vws_url) as mock: - mock.add_database(database=mock_database) + mock.add_cloud_database(cloud_database=mock_database) result = runner.invoke( cli=vws_group, args=commands, diff --git a/tests/test_vws_errors.py b/tests/test_vws_errors.py index 3c4e7515..ebf8b60e 100644 --- a/tests/test_vws_errors.py +++ b/tests/test_vws_errors.py @@ -1,6 +1,4 @@ -""" -Tests for how errors from VWS are handled by the CLI. -""" +"""Tests for how errors from VWS are handled by the CLI.""" import io import uuid @@ -9,16 +7,17 @@ from click.testing import CliRunner from freezegun import freeze_time from mock_vws import MockVWS -from mock_vws.database import VuforiaDatabase +from mock_vws.database import CloudDatabase from mock_vws.states import States from vws import VWS from vws_cli import vws_group -def test_target_id_does_not_exist(mock_database: VuforiaDatabase) -> None: +def test_target_id_does_not_exist(mock_database: CloudDatabase) -> None: """ - Commands which take a target ID show an error if that does not map to a + Commands which take a target ID show an error if that does not map + to a target in the database. """ runner = CliRunner() @@ -43,7 +42,8 @@ def test_target_id_does_not_exist(mock_database: VuforiaDatabase) -> None: def test_bad_image( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, tmp_path: Path, ) -> None: """An error is given when Vuforia returns a ``BadImage`` error. @@ -76,12 +76,15 @@ def test_bad_image( def test_fail_bad_request( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: - """An error is given when Vuforia returns a ``Fail`` error with a ``400`` - error code. For example, when the given server access key does not exist. + """An error is given when Vuforia returns a ``Fail`` error with a + ``400`` + error code. For example, when the given server access key does not + exist. With ``vws_python`` we cannot get a (guaranteed) 500 error or 422 response with a ``Fail`` error. @@ -114,13 +117,12 @@ def test_fail_bad_request( def test_metadata_too_large( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: - """ - An error is given when the given metadata is too large. - """ + """An error is given when the given metadata is too large.""" new_file = tmp_path / uuid.uuid4().hex new_file.write_bytes(data=high_quality_image.getvalue()) runner = CliRunner() @@ -147,13 +149,12 @@ def test_metadata_too_large( def test_image_too_large( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, png_too_large: io.BytesIO, tmp_path: Path, ) -> None: - """ - An error is given when the given image is too large. - """ + """An error is given when the given image is too large.""" runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = png_too_large.getvalue() @@ -184,13 +185,15 @@ def test_image_too_large( def test_target_name_exist( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, vws_client: VWS, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: """ - An error is given when there is already a target with the given name. + An error is given when there is already a target with the given + name. """ name = "foobar" vws_client.add_target( @@ -231,19 +234,21 @@ def test_target_name_exist( def test_project_inactive( + *, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: """ - An error is given if the project is inactive and the desired action cannot + An error is given if the project is inactive and the desired action + cannot be taken because of this. """ new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() new_file.write_bytes(data=image_data) - database = VuforiaDatabase(state=States.PROJECT_INACTIVE) + database = CloudDatabase(state=States.PROJECT_INACTIVE) with MockVWS() as mock: - mock.add_database(database=database) + mock.add_cloud_database(cloud_database=database) runner = CliRunner() commands = [ "add-target", @@ -273,13 +278,60 @@ def test_project_inactive( assert not result.stdout +def test_project_has_no_api_access( + *, + high_quality_image: io.BytesIO, + tmp_path: Path, +) -> None: + """ + An error is given if the project is not allowed to make API + requests. + """ + new_file = tmp_path / uuid.uuid4().hex + image_data = high_quality_image.getvalue() + new_file.write_bytes(data=image_data) + database = CloudDatabase(state=States.PROJECT_HAS_NO_API_ACCESS) + with MockVWS() as mock: + mock.add_cloud_database(cloud_database=database) + runner = CliRunner() + commands = [ + "add-target", + "--name", + "foo", + "--width", + "0.1", + "--image", + str(object=new_file), + "--server-access-key", + database.server_access_key, + "--server-secret-key", + database.server_secret_key, + ] + result = runner.invoke( + cli=vws_group, + args=commands, + catch_exceptions=False, + color=True, + ) + + assert result.exit_code == 1 + expected_stderr = ( + "Error: The request could not be completed because this database is " + "not allowed to make API requests.\n" + ) + assert result.stderr == expected_stderr + assert not result.stdout + + def test_unknown_vws_error( - mock_database: VuforiaDatabase, + *, + mock_database: CloudDatabase, high_quality_image: io.BytesIO, tmp_path: Path, ) -> None: """ - When an unknown VWS error is given, e.g. what is given when some bad names + When an unknown VWS error is given, e.g. what is given when some bad + names are given, an error is given. """ runner = CliRunner() @@ -318,12 +370,14 @@ def test_unknown_vws_error( def test_target_status_processing( + *, vws_client: VWS, high_quality_image: io.BytesIO, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: """ - An error is given when trying to delete a target which is processing. + An error is given when trying to delete a target which is + processing. """ runner = CliRunner() @@ -361,12 +415,14 @@ def test_target_status_processing( def test_target_status_not_success( + *, vws_client: VWS, high_quality_image: io.BytesIO, - mock_database: VuforiaDatabase, + mock_database: CloudDatabase, ) -> None: """ - An error is given when updating a target which has a status which is not + An error is given when updating a target which has a status which is + not "Success". """ runner = CliRunner() @@ -403,10 +459,8 @@ def test_target_status_not_success( assert not result.stdout -def test_authentication_failure(mock_database: VuforiaDatabase) -> None: - """ - An error is given when the secret key is incorrect. - """ +def test_authentication_failure(mock_database: CloudDatabase) -> None: + """An error is given when the secret key is incorrect.""" runner = CliRunner() commands = [ "list-targets", @@ -428,9 +482,10 @@ def test_authentication_failure(mock_database: VuforiaDatabase) -> None: assert not result.stdout -def test_request_time_too_skewed(mock_database: VuforiaDatabase) -> None: +def test_request_time_too_skewed(mock_database: CloudDatabase) -> None: """ - An error is given when the request time is more than 5 minutes different + An error is given when the request time is more than 5 minutes + different from the server time. """ runner = CliRunner() diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..0b8382c5 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2492 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15' and sys_platform != 'darwin'", + "python_full_version < '3.15' and sys_platform == 'darwin'", +] + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "actionlint-py" +version = "1.7.12.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/0b/3f29683dfbe94208fb5c3806806a6ef419972892e25c3c4f95198f68c978/actionlint_py-1.7.12.24.tar.gz", hash = "sha256:7571b0724fde79b2572b98b2b53792c470249d4db29951b57fc49b9cd3eaf11e", size = 12071, upload-time = "2026-03-31T06:21:35.015Z" } + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "astroid" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/c17d0f83016532a1ad87d1de96837164c99d47a3b6bbba28bd597c25b37a/astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3", size = 406224, upload-time = "2026-01-03T22:14:26.096Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/66/686ac4fc6ef48f5bacde625adac698f41d5316a9753c2b20bb0931c9d4e2/astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14", size = 276443, upload-time = "2026-01-03T22:14:24.412Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "build" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.15' and os_name == 'nt') or (os_name == 'nt' and sys_platform != 'darwin')" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "check-manifest" +version = "0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "build" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/e3/8ce797dfdf12d447683490107c4dcd97bb2535d7a2b031bf3f3e4c441c3d/check_manifest-0.51.tar.gz", hash = "sha256:9801c7637675755a563f33e3c48ee59a59b37a7677297c05c910c16c5b9b6d67", size = 36302, upload-time = "2025-10-15T11:15:48.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c1/df01ef6ba1c8a2bfc201be45d0889b06f165008b240e8d1657aa665421a8/check_manifest-0.51-py3-none-any.whl", hash = "sha256:f5f35ed561012fc2115bb070e42a748ac2e034cf8904ab4dfaae893859085ca4", size = 20500, upload-time = "2025-10-15T11:15:46.058Z" }, +] + +[[package]] +name = "check-wheel-contents" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "wheel-filename" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/37/2b9e0d38c2f668791ffe8b97711185c364d91c027e47f189c371c2348d18/check_wheel_contents-0.6.3.tar.gz", hash = "sha256:10e6939e2fe4e6ce1edf2ff6ec6157808677e80782e78021ae139dd88473a442", size = 586023, upload-time = "2025-08-02T14:01:45.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/05/f39fde9f31ef80b285ef5822fad4ddabf73fec62a1f02c5beb4b2f328972/check_wheel_contents-0.6.3-py3-none-any.whl", hash = "sha256:5ae39c8c434b972f0740d04610759168590713175aab584b012b1b84f6771874", size = 27541, upload-time = "2025-08-02T14:01:43.968Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "click-compose" +version = "2025.10.27.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/9f/7b380e5318643348e256ec31df1362b74dfa12733f76b1a97e1171ba74fe/click_compose-2025.10.27.3.tar.gz", hash = "sha256:6d3326a13b690ac7a0f0e99de785aa78ea81d130ba02d609e6367a7af23477a5", size = 18056, upload-time = "2025-10-27T11:49:45.228Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/3a/411c2ad30f87b2e874a4a4d1578dc9fe11a6ea8f139b4e1f7ff291a934ca/click_compose-2025.10.27.3-py2.py3-none-any.whl", hash = "sha256:6821fb769067e76d2b2e9c5d4d5e8d974002137322ab71cde65b10bf9c025834", size = 4731, upload-time = "2025-10-27T11:49:43.857Z" }, +] + +[[package]] +name = "cloup" +version = "3.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/cf/09a31f0f51b5c8ef2343baf37c35a5feb4f6dfdcbd0592a014baf837f2e4/cloup-3.0.8.tar.gz", hash = "sha256:f91c080a725196ddf74feabd6250266f466e97fc16dfe21a762cf6bc6beb3ecb", size = 229657, upload-time = "2025-08-05T02:25:02.83Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/0a/494a923f90cd97cdf4fb989cfd06ac0c6745f6dfb8adcef1b7f99d3c7834/cloup-3.0.8-py2.py3-none-any.whl", hash = "sha256:6fe9474dc44fa06f8870e9c797f005de1e3ef891ddc1a9612d9b58598a038323", size = 54647, upload-time = "2025-08-05T02:25:01.536Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[[package]] +name = "deptry" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "packaging" }, + { name = "requirements-parser" }, + { name = "tomli", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/b2/50ccc99362ae7757342978b7ecb3b98e47fade721fd617d74db1948ec3a1/deptry-0.25.1.tar.gz", hash = "sha256:45c8cd982c85cd4faae573ddff6920de7eec735336db6973f26a765ae7950f7d", size = 509748, upload-time = "2026-03-18T23:22:18.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/1d/b538dc635e873b25360d761cfe1fa0ccd7d6c69b698047e552f33401e60d/deptry-0.25.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a4dd1148db24a1ddacfa8b840836c6019c2f864fcb7579dd089fd217606338c8", size = 1850319, upload-time = "2026-03-18T23:22:15.65Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a9/511477a8f0ae4f6021d68a80bdca77e7ffb0722008dc24ee5d9ef49f5c88/deptry-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c67c666d916ef12013c0772e40d78be0f21577a495d8d99ec5fcb18c332d393d", size = 1759259, upload-time = "2026-03-18T23:22:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4b/c9f0bdda410912a6df79a789cb118fa29acae02a397794ead3c84adcda5c/deptry-0.25.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d39279828dbf4efc1abb40bf50a71b21499c36759bed5a8d8a3c0e3149b091", size = 1872012, upload-time = "2026-03-18T23:22:19.145Z" }, + { url = "https://files.pythonhosted.org/packages/72/9c/6f6f9125bac74b5d5d2af89536cbdb3fa159b6466aa097b74e7e85e8e030/deptry-0.25.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14bfcc28b4326ed8c6abb30691b19077d4ef8613cfba6c37ef5b1f471775bf6f", size = 1926575, upload-time = "2026-03-18T23:22:11.269Z" }, + { url = "https://files.pythonhosted.org/packages/52/48/2a5e705a7f898295966ade67bd1223e2af96da433e25b39f6b9483ba2c7b/deptry-0.25.1-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:555f5f9a487899ec9bf301eecba1745e14d212c4b354f4d3a5fd691e907366d3", size = 2050816, upload-time = "2026-03-18T23:22:27.439Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/50f189a894e1f3bf21266299112c8a06cb731838976e1b9a9cadd0b4a86e/deptry-0.25.1-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d21b3545ab2bfec53f3f45c6f5f201d55f713323327f8d12674505469ae6b7", size = 2145416, upload-time = "2026-03-18T23:22:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6a/3f82f7a06217778282bc4456af1b4ffb3bc4b2c8e7891d00e8323f9ad0b8/deptry-0.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:b59a560cb7dffb21832a98bb80d33d614cfb5630ea36ce21833eabf4eae3df99", size = 1718489, upload-time = "2026-03-18T23:22:28.589Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7f/cd6b3ac8cf95f2f1c5c7a74ff6452e9098af89a9b56607381f677880641e/deptry-0.25.1-cp310-abi3-win_arm64.whl", hash = "sha256:6efffd8116fb9d2c45a251382ce4ce1c38dbb17179f581ec9231ed5390f7fc12", size = 1647020, upload-time = "2026-03-18T23:22:23.311Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "doc8" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pygments" }, + { name = "restructuredtext-lint" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/91/88bb55225046a2ee9c2243d47346c78d2ed861c769168f451568625ad670/doc8-2.0.0.tar.gz", hash = "sha256:1267ad32758971fbcf991442417a3935c7bc9e52550e73622e0e56ba55ea1d40", size = 28436, upload-time = "2025-06-13T13:08:53.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/e9/90b7d243364d3dce38c8c2a1b8c103d7a8d1383c2b24c735fae0eee038dd/doc8-2.0.0-py3-none-any.whl", hash = "sha256:9862710027f793c25f9b1899150660e4bf1d4c9a6738742e71f32011e2e3f590", size = 25861, upload-time = "2025-06-13T13:08:51.839Z" }, +] + +[[package]] +name = "doccmd" +version = "2026.7.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "charset-normalizer" }, + { name = "click" }, + { name = "click-compose" }, + { name = "cloup" }, + { name = "dulwich" }, + { name = "pygments" }, + { name = "sybil" }, + { name = "sybil-extras" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/1a1c30b66d9dd37d1d12e248943a8723da79b5c7d945bba5c0e469252433/doccmd-2026.7.19.tar.gz", hash = "sha256:1bb47f9ba5a3aaa907a5b52cd42f62d718689eccef68b94740127f7b758ce573", size = 196169, upload-time = "2026-07-19T13:56:40.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/ef/c97a0d16c9c2f0870811e9b03548f2aa3fc3e1e39fe80b768fa8458d6d8a/doccmd-2026.7.19-py3-none-any.whl", hash = "sha256:32d1bd7dfd87ebe28b7cf7637413012b2909514e7da3400de5dcc1cb18da1235", size = 24701, upload-time = "2026-07-19T13:56:39.117Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "dom-toml" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "domdf-python-tools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/f6/edebb9290d3889d8ed460338dff752ab3764ec0727d81399e9328f38ac36/dom_toml-2.2.0.tar.gz", hash = "sha256:098d01b666bad39aa3d8cd916eea1c73c431a69e621511d3119c001604f27d9f", size = 12697, upload-time = "2026-01-02T21:19:05.839Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/d1/1a4664c9f615c5e6a56a2fc59e5f5bc1e89408af81955939e5b0eab8c02e/dom_toml-2.2.0-py3-none-any.whl", hash = "sha256:66455194679c2e77e5e3d97401f0f20e06a34b1506610d312eebef94fe198bc3", size = 17033, upload-time = "2026-01-02T21:19:04.943Z" }, +] + +[[package]] +name = "domdf-python-tools" +version = "3.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "natsort" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/8b/ab2d8a292bba8fe3135cacc8bfd3576710a14b8f2d0a8cde19130d5c9d21/domdf_python_tools-3.10.0.tar.gz", hash = "sha256:2ae308d2f4f1e9145f5f4ba57f840fbfd1c2983ee26e4824347789649d3ae298", size = 100458, upload-time = "2025-02-12T17:34:05.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/11/208f72084084d3f6a2ed5ebfdfc846692c3f7ad6dce65e400194924f7eed/domdf_python_tools-3.10.0-py3-none-any.whl", hash = "sha256:5e71c1be71bbcc1f881d690c8984b60e64298ec256903b3147f068bc33090c36", size = 126860, upload-time = "2025-02-12T17:34:04.093Z" }, +] + +[[package]] +name = "dulwich" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/df/4178b6465e118e6e74fd78774b451953dd53c09fdec18f2c4b3319dd0485/dulwich-1.0.0.tar.gz", hash = "sha256:3d07104735525f22bfec35514ac611cf328c89b7acb059316a4f6e583c8f09bc", size = 1135862, upload-time = "2026-01-17T23:44:16.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/65/e18082b56f9bed8e07f0632fbf3b9fd29d31d66dc3329359fe6577c98741/dulwich-1.0.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1bd4c872b617edc20e0b726479ec25705a0107a4bd16f83cb6cd108d86950634", size = 1435705, upload-time = "2026-01-17T23:44:10.991Z" }, + { url = "https://files.pythonhosted.org/packages/27/61/4974b3f593c3c1f7cf3656ca184246261632b81a27827ca453956d7b817e/dulwich-1.0.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc791771859258ffbe34a6579ece30684a535ae0341fd2f5adfa5f64cc885c13", size = 1435697, upload-time = "2026-01-17T23:44:12.708Z" }, + { url = "https://files.pythonhosted.org/packages/f9/99/4543953d2f7c1a940c1373362a70d253b85860be64b4ef8885bf8bfb340b/dulwich-1.0.0-py3-none-any.whl", hash = "sha256:221be803b71b060c928e9faae4ab3e259ff5beac6e0c251ba3c176b51b5c2ffb", size = 647950, upload-time = "2026-01-17T23:44:14.449Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "freezegun" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + +[[package]] +name = "furo" +version = "2025.12.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hadolint-bin" +version = "2.15.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/2d/61dd58ec15986fa29204564438d82bdc46eec885e9f3049a3936ad3b571a/hadolint_bin-2.15.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:df1696d766373cac8f2fe0938230d65efb9348557816f402868e0769de62916c", size = 4897923, upload-time = "2026-08-03T17:43:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f5/6ee242b698bc38b02730c718f6e12d898651f44c96ff574ddef606c27644/hadolint_bin-2.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2dbbf293728f1e4232bb3f8a8165009ce27f2f70957381eeb5e501d08679769c", size = 21349339, upload-time = "2026-08-03T17:43:48.839Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/61cfdc0ccd2e43865249a25c02db8e54115ba97eae178b619ce1af527926/hadolint_bin-2.15.1-py3-none-manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9d810140051c6068d002202a420e5705ffa9fff42e7fc0f54e67f90e6845723d", size = 13062348, upload-time = "2026-08-03T17:43:50.979Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/2d98b50d966b32e4e6f71ac429bd90bd1c2390ce4cd7dcc1056df9c233a2/hadolint_bin-2.15.1-py3-none-manylinux2014_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:bed607e611b0f7e4ef78e4bba9bdf39504d8234d3eadfe86d702f2bff14ac2d5", size = 12154446, upload-time = "2026-08-03T17:43:54.75Z" }, +] + +[[package]] +name = "homebrew-pypi-poet" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/d9/4b525af3be6ac0a0a962e101b7771db6511d9e96369ded2765406233f9ff/homebrew-pypi-poet-0.10.0.tar.gz", hash = "sha256:e09e997e35a98f66445f9a39ccb33d6d93c5cd090302a59f231707eac0bf378e", size = 5953, upload-time = "2018-02-23T06:22:55.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/85/998232eae0b5c6798c7140ef37d2c1be02ea06cd38dd80169b3abd63b600/homebrew_pypi_poet-0.10.0-py2.py3-none-any.whl", hash = "sha256:65824f97aea0e713c4ac18aa2ef4477aca69426554eac842eeaaddf97df3fc47", size = 7813, upload-time = "2018-02-23T06:22:54.858Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "interrogate" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "colorama" }, + { name = "py" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/22/74f7fcc96280eea46cf2bcbfa1354ac31de0e60a4be6f7966f12cef20893/interrogate-1.7.0.tar.gz", hash = "sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0", size = 159636, upload-time = "2024-04-07T22:30:46.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl", hash = "sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12", size = 46982, upload-time = "2024-04-07T22:30:44.277Z" }, +] + +[[package]] +name = "isort" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "maison" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "loguru" }, + { name = "platformdirs" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/45/7cb1d08b6b5674c381b6e0232d35f417a1eba8bb66cdc18edff2b9c80b68/maison-2.0.2.tar.gz", hash = "sha256:476f2bf414a20f5abf5a9856bd4db78b5a33c695654a0fc49c3c4abed78c2efc", size = 16012, upload-time = "2025-10-09T07:52:33.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/8f/3f0895a18cad5afd61c16ac38d35a2466f0cac8ae5c28f1a67f7a81bcdec/maison-2.0.2-py3-none-any.whl", hash = "sha256:835de804aa8063795b48c4fe2b4918106cfda4e5df515e8784ec9fa64cd28191", size = 13464, upload-time = "2025-10-09T07:52:31.987Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[package.optional-dependencies] +faster-cache = [ + { name = "orjson" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "mypy-strict-kwargs" +version = "2026.7.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/ff/c7100891e45af1ce05db4644ea4a6e3f53aa6a019ab9defecd5fde9e7d69/mypy_strict_kwargs-2026.7.19.1.tar.gz", hash = "sha256:735a1956937365daaea84a6a3a556fde7255575e6fabc5336e499bb890abe44f", size = 30368, upload-time = "2026-07-19T11:59:40.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/8a/0f55537fcfe8d358f681059be0ea158df2066d629215cd9090eae4937e17/mypy_strict_kwargs-2026.7.19.1-py3-none-any.whl", hash = "sha256:80c56a7792bb4f0880f7686a8f576022514b219ff8c7c1a8c6e5096c0d64cae2", size = 14064, upload-time = "2026-07-19T11:59:39.009Z" }, +] + +[[package]] +name = "myst-parser" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, +] + +[[package]] +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "no-defaults" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/dd/37d0ae30b5d74d039d9e9c1b0e7dd5ebee509fe848314825505f2798a444/no_defaults-2.1.0.tar.gz", hash = "sha256:d98d78c7dace794bed068fbbada2ee7e94705141aa17c2a8d7047bacb3da2562", size = 96084, upload-time = "2026-08-07T12:35:34.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/5e/2efb07ac90e5db3623a6ee6fbacbd3635d4d63c359cfc94ed39cca685649/no_defaults-2.1.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:94b884e31e7727f6a476b6dff0b655fd37b65f6f209ddccba917888260050ca0", size = 2141080, upload-time = "2026-08-07T12:35:30.314Z" }, + { url = "https://files.pythonhosted.org/packages/1d/be/33eacae5d5b420df33a9b5a716eae4a8c3528a795e6435cbcf7fe00d0041/no_defaults-2.1.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:bed135d188635d786cca6dde75982d507f8e2b46fbc17f047c794af2153186d3", size = 2328857, upload-time = "2026-08-07T12:35:31.861Z" }, + { url = "https://files.pythonhosted.org/packages/35/a2/8cdeb0081d019275c86a1e120aa744b13de6ca4260162b6c41215fa0e007/no_defaults-2.1.0-py3-none-win_amd64.whl", hash = "sha256:d14855c701f5e1310e304e3011cbc5836fa19e7494125e6673dd14701fc49b7e", size = 2052526, upload-time = "2026-08-07T12:35:33.435Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, +] + +[[package]] +name = "opencv-contrib-python-headless" +version = "5.0.0.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/85/da534b90d99a040fbdccc6eb2c3d1c6c6d0e1f7d54732f4ce7d2e1726b29/opencv_contrib_python_headless-5.0.0.93.tar.gz", hash = "sha256:6a8c34de905f59b038f6ab0475e73fb0a0f345c3582ec3aeaeb1c0912e80cddb", size = 154148527, upload-time = "2026-07-02T06:59:00.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b9/2a3a9e23d7894f816792f93f2e73c017765a29e854ba343cb260da400a7e/opencv_contrib_python_headless-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:bf1e2b3c502b4fbe7b06e307bd15c4e7bea7da8895de19c189db01d53062f997", size = 55653233, upload-time = "2026-07-02T05:50:39.358Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/1b7d64b03e54f775035bda1dc363dfde89e9301a9bdf82949b239ddf80ad/opencv_contrib_python_headless-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:68eb7b803f68998552a83b5f3c69bfb1c7d6c114dc30f828657bddc7ca68573a", size = 42828135, upload-time = "2026-07-02T05:51:44.374Z" }, + { url = "https://files.pythonhosted.org/packages/61/14/f12a54f7e5e8783adf905bb863ef2257e41d73d44793851d318e35ee8121/opencv_contrib_python_headless-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c035e12b6078b3351ad577e90a4ff4ead74155fa5470eb8df8f5b54a8ce7727f", size = 43800290, upload-time = "2026-07-02T06:51:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c0/a51fd47d4f82cbc0bb36d082c87f6da25e4fb2a1cdec307a72f063cf3d55/opencv_contrib_python_headless-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ff190c4ebfa2839bdcaff1810061bc82f9ce5cf37e28953fb57fa5ed519b9d1", size = 64676895, upload-time = "2026-07-02T06:52:01.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/78/11f0704e94ebf92f748c789cf5d430d4459ae3649263c712d81772cc4360/opencv_contrib_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01331ae1a65a21d81310b5584900f73c3012ab4bdcf6aa26085c1870eea45da8", size = 44443761, upload-time = "2026-07-02T06:52:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/3a803f04a28d0161fd9f5e085595507d7008d760ba95be6afa5760a41bbf/opencv_contrib_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:63ddadd47e36fbc903d5ac141d5c90bd305a8044188dd4936a73de80007c9e6c", size = 69475842, upload-time = "2026-07-02T06:52:49.523Z" }, + { url = "https://files.pythonhosted.org/packages/30/9f/11dd169d7a037c35b1fec0eb766c07bc62cde6428f2762df233111a43204/opencv_contrib_python_headless-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:ca47e569680df6eb7317b54f463bc766832d3d87854988168673bb0c52d6d9d4", size = 44203828, upload-time = "2026-07-02T05:50:20.943Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f3/6d7a7e512d88df0358c09c36cfac1a9050c50df547c64d32dc5f82fade49/opencv_contrib_python_headless-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:72605e3ae78f66592705b1abe62e60f57dd9050787fdd99711b85a02760d5259", size = 53654741, upload-time = "2026-07-02T05:50:17.153Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/a3/4e09c61a5f0c521cba0bb433639610ae037437669f1a4cbc93799e731d78/orjson-3.11.6.tar.gz", hash = "sha256:0a54c72259f35299fd033042367df781c2f66d10252955ca1efb7db309b954cb", size = 6175856, upload-time = "2026-01-29T15:13:07.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/9f/46ca908abaeeec7560638ff20276ab327b980d73b3cc2f5b205b4a1c60b3/orjson-3.11.6-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6026db2692041d2a23fe2545606df591687787825ad5821971ef0974f2c47630", size = 249823, upload-time = "2026-01-29T15:12:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/ff/78/ca478089818d18c9cd04f79c43f74ddd031b63c70fa2a946eb5e85414623/orjson-3.11.6-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:132b0ab2e20c73afa85cf142e547511feb3d2f5b7943468984658f3952b467d4", size = 134328, upload-time = "2026-01-29T15:12:45.171Z" }, + { url = "https://files.pythonhosted.org/packages/39/5e/cbb9d830ed4e47f4375ad8eef8e4fff1bf1328437732c3809054fc4e80be/orjson-3.11.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b376fb05f20a96ec117d47987dd3b39265c635725bda40661b4c5b73b77b5fde", size = 137651, upload-time = "2026-01-29T15:12:46.602Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3a/35df6558c5bc3a65ce0961aefee7f8364e59af78749fc796ea255bfa0cf5/orjson-3.11.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:954dae4e080574672a1dfcf2a840eddef0f27bd89b0e94903dd0824e9c1db060", size = 134596, upload-time = "2026-01-29T15:12:47.95Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8e/3d32dd7b7f26a19cc4512d6ed0ae3429567c71feef720fe699ff43c5bc9e/orjson-3.11.6-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe515bb89d59e1e4b48637a964f480b35c0a2676de24e65e55310f6016cca7ce", size = 140923, upload-time = "2026-01-29T15:12:49.333Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/1efbf5c99b3304f25d6f0d493a8d1492ee98693637c10ce65d57be839d7b/orjson-3.11.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:380f9709c275917af28feb086813923251e11ee10687257cd7f1ea188bcd4485", size = 144068, upload-time = "2026-01-29T15:12:50.927Z" }, + { url = "https://files.pythonhosted.org/packages/82/83/0d19eeb5be797de217303bbb55dde58dba26f996ed905d301d98fd2d4637/orjson-3.11.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8173e0d3f6081e7034c51cf984036d02f6bab2a2126de5a759d79f8e5a140e7", size = 142493, upload-time = "2026-01-29T15:12:52.432Z" }, + { url = "https://files.pythonhosted.org/packages/32/a7/573fec3df4dc8fc259b7770dc6c0656f91adce6e19330c78d23f87945d1e/orjson-3.11.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dddf9ba706294906c56ef5150a958317b09aa3a8a48df1c52ccf22ec1907eac", size = 145616, upload-time = "2026-01-29T15:12:53.903Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0e/23551b16f21690f7fd5122e3cf40fdca5d77052a434d0071990f97f5fe2f/orjson-3.11.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cbae5c34588dc79938dffb0b6fbe8c531f4dc8a6ad7f39759a9eb5d2da405ef2", size = 146951, upload-time = "2026-01-29T15:12:55.698Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/5e6c8f39805c39123a18e412434ea364349ee0012548d08aa586e2bd6aa9/orjson-3.11.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f75c318640acbddc419733b57f8a07515e587a939d8f54363654041fd1f4e465", size = 421024, upload-time = "2026-01-29T15:12:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4d/724975cf0087f6550bd01fd62203418afc0ea33fd099aed318c5bcc52df8/orjson-3.11.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e0ab8d13aa2a3e98b4a43487c9205b2c92c38c054b4237777484d503357c8437", size = 155774, upload-time = "2026-01-29T15:12:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/f4c4e3f46b55db29e0a5f20493b924fc791092d9a03ff2068c9fe6c1002f/orjson-3.11.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f884c7fb1020d44612bd7ac0db0babba0e2f78b68d9a650c7959bf99c783773f", size = 147393, upload-time = "2026-01-29T15:13:00.769Z" }, + { url = "https://files.pythonhosted.org/packages/ee/86/6f5529dd27230966171ee126cecb237ed08e9f05f6102bfaf63e5b32277d/orjson-3.11.6-cp314-cp314-win32.whl", hash = "sha256:8d1035d1b25732ec9f971e833a3e299d2b1a330236f75e6fd945ad982c76aaf3", size = 139760, upload-time = "2026-01-29T15:13:02.173Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b5/91ae7037b2894a6b5002fb33f4fbccec98424a928469835c3837fbb22a9b/orjson-3.11.6-cp314-cp314-win_amd64.whl", hash = "sha256:931607a8865d21682bb72de54231655c86df1870502d2962dbfd12c82890d077", size = 136633, upload-time = "2026-01-29T15:13:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/74/f473a3ec7a0a7ebc825ca8e3c86763f7d039f379860c81ba12dcdd456547/orjson-3.11.6-cp314-cp314-win_arm64.whl", hash = "sha256:fe71f6b283f4f1832204ab8235ce07adad145052614f77c876fcf0dac97bc06f", size = 135168, upload-time = "2026-01-29T15:13:05.932Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "polib" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/9a/79b1067d27e38ddf84fe7da6ec516f1743f31f752c6122193e7bce38bdbf/polib-1.2.0.tar.gz", hash = "sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b", size = 161658, upload-time = "2023-02-23T17:53:56.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/99/45bb1f9926efe370c6dbe324741c749658e44cb060124f28dad201202274/polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d", size = 20634, upload-time = "2023-02-23T17:53:59.919Z" }, +] + +[[package]] +name = "prek" +version = "0.4.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/79/19f47eeb4d6092d36f94f47a056e7ae7a421d60220c772e8513483521b63/prek-0.4.13.tar.gz", hash = "sha256:9bf3dce400ef38a281836e4fe6429aa5f1690848be77cd97cb53c93769db4681", size = 533200, upload-time = "2026-08-10T08:54:15.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/12/661dc1c63c322000580dffa8503d28d633cb5d4c3181e662cbb86eded12e/prek-0.4.13-py3-none-linux_armv6l.whl", hash = "sha256:6a313f5f041b2fcbd33bceb6b6e11ee9b8621c8c6ad8ef13787a6d90541b624d", size = 5801508, upload-time = "2026-08-10T08:53:52.18Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/c50597a45d08b22e5704a5d4d5c03269a581b46cf1f060861f9ba96cdade/prek-0.4.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:40436cd7247d2a2fc036ef07d7efcd828acf1aab9d648a8a3f21475e3ad3f789", size = 6141612, upload-time = "2026-08-10T08:53:53.86Z" }, + { url = "https://files.pythonhosted.org/packages/82/93/abc084bbd76bb6c34efbf7db441392f17b7bfc50a54c4d53d3e37c7ad6e0/prek-0.4.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:019a33b477b7b949fb6dcd6cb33ed494c4a28117e53c370c637ec0aba60211aa", size = 5625418, upload-time = "2026-08-10T08:53:55.39Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/a310ff9adcb4b822d935eb55f0f5cdcfe4c18a16610bc5e220a11215dc38/prek-0.4.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5a2851b6e60912e73be1bf2cf61fab5b314add15ba7bcf230f860282bdbaf16b", size = 5942132, upload-time = "2026-08-10T08:53:56.789Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d8/811230ff285000abdc0fb98b56b9ef2d23b2dc530cf2da86fe1665f844ba/prek-0.4.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ba9b94bd3f47b5f94a4ae504c44f9529cb3258ebaf577a5a6070ea0c6a853d6", size = 5710181, upload-time = "2026-08-10T08:53:58.278Z" }, + { url = "https://files.pythonhosted.org/packages/74/84/01f7163cc4267daba6b01cfac726327c6d848d2bb7349bd2fd877d88f744/prek-0.4.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33fc9e0d435cb9e650ec108f840febe7a94fd266c77149860091cead212dc82d", size = 6157705, upload-time = "2026-08-10T08:53:59.586Z" }, + { url = "https://files.pythonhosted.org/packages/30/e3/061a0e9ebd7064edf70bb783afa575d045ac0cc9035c2143b76401ebcaf5/prek-0.4.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91066d8978eab83c111e7c34ee3046a003819d2df15ef7dc2bddeb83dce62bc0", size = 6898016, upload-time = "2026-08-10T08:54:00.927Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/5e8e2b9ff6a30b46bf35ca3b50e4d164c26406d9e7457d5b4633c6e1fbc0/prek-0.4.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85646fcc940f30bd946d63b6bbd1000d946994c88e154c5bdde7773a2c358dcf", size = 6365056, upload-time = "2026-08-10T08:54:02.704Z" }, + { url = "https://files.pythonhosted.org/packages/f9/23/afe543b69fb7f35016645eb4b766191e814cbcfa5545d695a2a7dd9b712e/prek-0.4.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:961859c3ddddb8e10367afcf93742da36fa213eda080f168fc1dcbe33e0b004d", size = 5952174, upload-time = "2026-08-10T08:54:04.079Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/9e651ce51aa0b03244277f5e0660cf1b946a270cb62635a8a100389a67a2/prek-0.4.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ccf4fcbfc686ad6b589f2908ed6012a315091c45163e1f4420648b9669651a9e", size = 5761875, upload-time = "2026-08-10T08:54:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/cf/64/e70e18734d93df272476d2f1b30d92c71d41ee7141070f1dd13cfbb2eff8/prek-0.4.13-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:eb4b7edfe00ca73e58d7e26fb871abddc5854e8d21067a202e43418fb7f98ef2", size = 5685365, upload-time = "2026-08-10T08:54:06.729Z" }, + { url = "https://files.pythonhosted.org/packages/de/78/686fd5d3f12249368a0fdd8f7705e3ba540402a6157ef0b888e48734ff83/prek-0.4.13-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c29449443f89da1647331742984b7046a55084759ad642373a9cc0a973494339", size = 5999013, upload-time = "2026-08-10T08:54:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/9e/aa/93ffac2460b44f6182dbbb3194db976d18332edcb8208c4a796f8f9955f8/prek-0.4.13-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:7a04d5ac901819b115ecd0bf79cee091f0034323767abcc4a53626eeb96c3be0", size = 6486759, upload-time = "2026-08-10T08:54:09.775Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/9f12c0d469ea345c249d5b0a027a1f2bf1ca05041d7785c34c455a9b605c/prek-0.4.13-py3-none-win32.whl", hash = "sha256:6d1bdcc1699ae18270f9bac9c4b4d29c6f1512b7a067e17ce5e220c30636f88c", size = 5515046, upload-time = "2026-08-10T08:54:11.456Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3c/ef9ec67c560e60525d6a49b48a2a60434906f25ec2efa5e010fe2d42bbfa/prek-0.4.13-py3-none-win_amd64.whl", hash = "sha256:2d8fd796ed7944154fbee6d5a6d2490b9d4f14ce3626b1a0c9ca698455b25d9b", size = 5894683, upload-time = "2026-08-10T08:54:12.88Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/8fe8fdf8154108a552d5578400da44144697ed11e5bd71d5a4980d7e202e/prek-0.4.13-py3-none-win_arm64.whl", hash = "sha256:65d6811b0220444bcdf539e157d7d5cb8edab01a5ed89534c70d73d675266ecb", size = 5650616, upload-time = "2026-08-10T08:54:14.21Z" }, +] + +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pydocstringformatter" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/c9/435887301c667ddcf1ed524ba82ff0998c280077f4987da6f3f72cb43778/pydocstringformatter-1.0.0.tar.gz", hash = "sha256:0c2bc5e200ff118feab96c204b1f69ddb604832e177b26b598939e0e6913dcd0", size = 29543, upload-time = "2026-07-04T16:39:24.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/ee/8acba0b2f928bd046ce06eef9c8f0450a3c25711f17b8362e6ed9b473da7/pydocstringformatter-1.0.0-py3-none-any.whl", hash = "sha256:3f625f91798b14ee7c4fc8e1628c2d040390e32efa76f05ef14838e9bc2d1724", size = 30188, upload-time = "2026-07-04T16:39:23.095Z" }, +] + +[[package]] +name = "pyenchant" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/ad/64925c937e41be75c7067c85757b3d45b148e9111187b37693269f583156/pyenchant-3.3.0.tar.gz", hash = "sha256:825288246b5debc9436f91967650974ef0d5636458502619e322c476f1283891", size = 60696, upload-time = "2025-09-14T16:23:12.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/b0/35926bad6885fb7bc24aa7e1b45e6d86540c6c57ee4abc4fed1ef58d4ec0/pyenchant-3.3.0-py3-none-any.whl", hash = "sha256:3da00b1d01314d85aac733bb997415d7a3e875666dc81735ddcf320aa36b7a70", size = 58363, upload-time = "2025-09-14T16:23:04.297Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7f/1d7b8ad86c2a841d940df7b965fa727e052b95d539e4c563da685c25d0d2/pyenchant-3.3.0-py3-none-win32.whl", hash = "sha256:1d55e075645a6edbb3c590fb42f9e02b4d455e4affe28a2227d5cb6d4868e626", size = 37787278, upload-time = "2025-09-14T16:23:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ae/5624803b62ecb0a20248f0d28ed3f78c78746a032582a016d4b2890c7899/pyenchant-3.3.0-py3-none-win_amd64.whl", hash = "sha256:04a5bd0e022ebe2e8c6d9e498ec3d650602e264ec5486e9c6a1b7f99c9507c49", size = 37427576, upload-time = "2025-09-14T16:23:09.574Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pylint" +version = "4.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/92/98dace02f2d11b88160354c53944f77ea7327aa78bce1c75971e7aaa4347/pylint-4.0.7.tar.gz", hash = "sha256:9b2d1d15791c84b77a4fe2aafe8f0d9570717e2dea06d53b19c105cf60275a52", size = 1594770, upload-time = "2026-08-09T19:13:23.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/b0/3a8040e53df6c5c1e04b0e23ed53fdbeb64f333723a334d313fba2f581ce/pylint-4.0.7-py3-none-any.whl", hash = "sha256:be4a3111557a614411ed1fc89347ce4a8e1013a59e1f33d11485227a02e3304d", size = 539710, upload-time = "2026-08-09T19:13:21.228Z" }, +] + +[package.optional-dependencies] +spelling = [ + { name = "pyenchant" }, +] + +[[package]] +name = "pyproject-fmt" +version = "2.27.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/39/65b0c0342498ca594f1f678e9bb955bde32ba75ffabf8dff6c711d703109/pyproject_fmt-2.27.0.tar.gz", hash = "sha256:31f638e1d42a6689922d9c413d410a5f3f56e45c844830763321119e919cd45b", size = 300145, upload-time = "2026-08-03T22:54:59.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/41/2ef4c9e092a4c3e7c70311eb2cfd4125fac742734db708ab5c00cef62483/pyproject_fmt-2.27.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0a780b411258f5ee62b9c622c0c07f55053e64e0866cfede5648b99b7573ec84", size = 5344637, upload-time = "2026-08-03T22:54:07.039Z" }, + { url = "https://files.pythonhosted.org/packages/69/56/4f88cfd3f4e83bd6ba16c0a9b0d9776efbeb4bd504fb661da5d9344790a6/pyproject_fmt-2.27.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:62acfbfe9dc150e542d0764aed71e901f6f2d818e3a9c07ee5176306348db146", size = 5102853, upload-time = "2026-08-03T22:54:08.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/93/84b226f18da04a3ef21308f8a310b859e96c44cbeb88723f0409d70eb411/pyproject_fmt-2.27.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:754f840e915861a594ab402c1c62e09c7a53692e5db57eb544a6731154c674ce", size = 5273151, upload-time = "2026-08-03T22:54:10.75Z" }, + { url = "https://files.pythonhosted.org/packages/7f/79/d6d47dc82e39a759819af2cde51adfefcef9cff32a421245f32b18393ecf/pyproject_fmt-2.27.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:97c803bd973d5c8a37a368a7c6d571ff09817570bc48b23b286ab3c55712c124", size = 5667236, upload-time = "2026-08-03T22:54:12.448Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/2e2c1a3a558be20b6fdf4c415fed97e5da32d3232ae90f84091a3b00a486/pyproject_fmt-2.27.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:dcdd93f1c1cab93b79ed6777ef729ac7961f3b1e8a7a0b7ff04b4b826a016b96", size = 5365689, upload-time = "2026-08-03T22:54:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/50/9f/b57e8ad891ea9f5fa6afec3c2d25d7d8cc8b8725cfd91381ff9b08baf8d4/pyproject_fmt-2.27.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d274ec865d4af811b874e85f23cfdc332ab05f2a66c5a6e6d10d3ff689462b5d", size = 5272980, upload-time = "2026-08-03T22:54:16.335Z" }, + { url = "https://files.pythonhosted.org/packages/56/61/914c9c4957b2e944c84adf211d6a8f28554bcc97b553de170a81aca5e96e/pyproject_fmt-2.27.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3fc9615a0b438c2cf9b7d233a9235b5889fd3cc9f7376e90418bce5f4f2acc8", size = 5842234, upload-time = "2026-08-03T22:54:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/90/f8/f16f7cc82eca680997cb36fb175a3a2a9246b47a1d90d718d59baf0f111b/pyproject_fmt-2.27.0-cp310-abi3-win_amd64.whl", hash = "sha256:3b44079ecf57addebfb06c8cbba5961d69eaea8e5dc5a21b781804c0c49a48b7", size = 5546145, upload-time = "2026-08-03T22:54:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/33/79/4d3fdfbdf4afe879dab75555af1387fca77f0f3397c36d79f9e3092d09d2/pyproject_fmt-2.27.0-cp310-abi3-win_arm64.whl", hash = "sha256:b226c744ab4d1a6918ecbccbcc2d73d0e7a3527d73c44dc8c5a3775c56dc0dd0", size = 5068780, upload-time = "2026-08-03T22:54:22.377Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/b621c3cbc19eb81d2473aab02c09bd2418929f3d61d9f3c92f127f285ba4/pyproject_fmt-2.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ce05601d0d266587a8cd08af6e1cc19935e1dc3be7d6f83fa20537d6a242512d", size = 5344535, upload-time = "2026-08-03T22:54:24.127Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/f7e51e7f59d586e0797de2f533029c71cd888025b33d51dbb3967ffd2f10/pyproject_fmt-2.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:97901311988fd3601df115af63cd285317ad60b4db79c4598efcbe9f8a7816c2", size = 5097006, upload-time = "2026-08-03T22:54:25.99Z" }, + { url = "https://files.pythonhosted.org/packages/e1/13/cd92d6bab4f1075425982e3e6c85bc300e320c9e8e53e96028386192ef73/pyproject_fmt-2.27.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:624a4aa2f4553697dd078f1c00edc8659a5e5ccfd07325611358332f0a5dcf13", size = 5267186, upload-time = "2026-08-03T22:54:27.869Z" }, + { url = "https://files.pythonhosted.org/packages/76/99/948a3f766935723b17143ee23bdf15b7b35bc9248fb14cbd019ece93ce2a/pyproject_fmt-2.27.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cdec6b19d887bb27df0a06448e9d0a4c3366c4a0e5b6ad308262d379a904d337", size = 5662485, upload-time = "2026-08-03T22:54:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/7700d5c1a123ed76289b673f68f2f558faddeea4f60b2d58634acd91497c/pyproject_fmt-2.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00833606878a5cf8e0d980e9175788a5b6d44a0d68d88d5723ec2befafddda06", size = 5266508, upload-time = "2026-08-03T22:54:32.135Z" }, + { url = "https://files.pythonhosted.org/packages/31/46/4b177d5606d3632a8690bc8489ae34984e712e58aa52496c811f1706d671/pyproject_fmt-2.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48d5f4da35d77905bf2234f12b787fecb2247d0573793f31c075a1469333bf39", size = 5837669, upload-time = "2026-08-03T22:54:34.081Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9d/c13f03014b2538a515caa83b6a41ef08b29f6ee6af7ac3cd75f0732384a6/pyproject_fmt-2.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:cc014e22b1e10d5f0d7b51faa281d310f04a646607ed4e620cf031bd5d7f57e8", size = 5543009, upload-time = "2026-08-03T22:54:36.049Z" }, + { url = "https://files.pythonhosted.org/packages/0b/21/341bc93ce40488a0e49bfc45e3328e11d4011ce655f2f45bd1f91f989199/pyproject_fmt-2.27.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3e104c0a28212af3e2b4228b04c83220c762ce4f0bd0ec7bb2f2230361a7f154", size = 5064853, upload-time = "2026-08-03T22:54:38.72Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/34cef5210591b288e0cc44138ecd4549ca93e933e11c26d4449129d34ae0/pyproject_fmt-2.27.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:f84843bfc074b8defa4376113a9d17b9d5f7ec10c5c5185662427409b4b9fea9", size = 5344052, upload-time = "2026-08-03T22:54:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/2ec9937459827018bb6983e29a5c6f62d519ae3019b359807c068cd2ca46/pyproject_fmt-2.27.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3291f2526bb0071801dd1734ba10062f7363a82123a61810aebf2d823a6bc52b", size = 5096312, upload-time = "2026-08-03T22:54:42.57Z" }, + { url = "https://files.pythonhosted.org/packages/45/14/fbaa36049d3aaec9f82b83d699cdb9fc2a46dad2ca740168111c312b4cb2/pyproject_fmt-2.27.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:0d7f55de0460109fb892323384ce1bb6cde5b3756a7c62b1465884b42ac648ed", size = 5268557, upload-time = "2026-08-03T22:54:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/95/c9/dbffa555dd0686c1309e72274d1fb5ad2feaed40469caeec62f5dc32717a/pyproject_fmt-2.27.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:9960ec75a2e1275c7519309838e196cbd2863db918ac8d6cf2edb89d715dbfc3", size = 5662628, upload-time = "2026-08-03T22:54:46.514Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a5/a41d21f984207e719cf338165440758b561ecd6875c5f682cfe578b9424b/pyproject_fmt-2.27.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:880f3002f702e8c888fd4b129ca699d318ebc3266ed12cc7a2f7b411e56d641f", size = 5267349, upload-time = "2026-08-03T22:54:49.068Z" }, + { url = "https://files.pythonhosted.org/packages/76/08/a51aa810d4a5518bfd8cb5bf49f55864a7cf88f5aa190fcf8627c6364c80/pyproject_fmt-2.27.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8a6e68a0780828853850c0e284fd67e75c14e7e40d681894f40bca5d17b0d93b", size = 5837708, upload-time = "2026-08-03T22:54:50.846Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pyrefly" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" }, + { url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" }, + { url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" }, + { url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.411" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, +] + +[[package]] +name = "pyroma" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "build" }, + { name = "docutils" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/68/a91ab78e5d7ff88eaaa10cefc948a07723d263e9a443e3650b32fbf0ac01/pyroma-5.0.1.tar.gz", hash = "sha256:703ae972e53e16be836966d03cd387906ecf32d64992345a61f2ed15805aee56", size = 67392, upload-time = "2025-12-09T10:10:22.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/cd/300d42aa7675d2ce66fea380177c19ad9f8ffad01295160a06e257360d73/pyroma-5.0.1-py3-none-any.whl", hash = "sha256:e71fd3e0f213b36870a607eccf491241dbadf5462ec1cdda94d08bfa1c26951e", size = 23012, upload-time = "2025-12-09T10:10:20.578Z" }, +] + +[[package]] +name = "pyteenybrisque" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/43/294f291398c93ed695ef208333857394763e50581da3fcef4a3aded3228f/pyteenybrisque-0.1.1.tar.gz", hash = "sha256:eb804b121146056ec6b6d08581f6f19983b565df11e746af0ebe802575c5e56e", size = 182546, upload-time = "2026-05-03T19:28:40.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/e6/40048a4eb960ee9fc48f7cade4d98274dac591ecc733beacfb3babacf703/pyteenybrisque-0.1.1-py3-none-any.whl", hash = "sha256:d3437290463c62c8479300fad0ab579e1357ce5bfc967e299a4916e24877a426", size = 182791, upload-time = "2026-05-03T19:28:39.587Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-beartype-tests" +version = "2026.4.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/4f/14167cd06fa425dc70eb486942e5ef402c738984726703584bba55353809/pytest_beartype_tests-2026.4.26.tar.gz", hash = "sha256:a986f59466243b616606279b3c6c00e7e171ee0b5aad4702922ea20fd1a1b52c", size = 88278, upload-time = "2026-04-26T17:12:24.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/2b/b9c1799582a00a9746f875deecd4449d0dc30254600c77231837bb67c6ec/pytest_beartype_tests-2026.4.26-py3-none-any.whl", hash = "sha256:e0c19a6708a14e4f97acb18ec94151550d70f736733b36d6cd621a6481cea99d", size = 5718, upload-time = "2026-04-26T17:12:22.889Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-datadir" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/46/db060b291999ca048edd06d6fa9ee95945d088edc38b1172c59eeb46ec45/pytest_datadir-1.8.0.tar.gz", hash = "sha256:7a15faed76cebe87cc91941dd1920a9a38eba56a09c11e9ddf1434d28a0f78eb", size = 11848, upload-time = "2025-07-30T13:52:12.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/7a/33895863aec26ac3bb5068a73583f935680d6ab6af2a9567d409430c3ee1/pytest_datadir-1.8.0-py3-none-any.whl", hash = "sha256:5c677bc097d907ac71ca418109adc3abe34cf0bddfe6cf78aecfbabd96a15cf0", size = 6512, upload-time = "2025-07-30T13:52:11.525Z" }, +] + +[[package]] +name = "pytest-regressions" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "pytest-datadir" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/0f/1d6b8cc851596be52c401af53fd0d844e72921b0b11866b88995673cd320/pytest_regressions-2.11.0.tar.gz", hash = "sha256:d4a86092f979eb25d2403c51b2b61039781c4c7c7a364a56b9fa838872a650f7", size = 117474, upload-time = "2026-05-25T12:09:01.796Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/61/fe772eda66bb8e2ee72da2937382aa4f803ca1e41092ca8d59953e96262c/pytest_regressions-2.11.0-py3-none-any.whl", hash = "sha256:fcb2bbcfb2fda256624d434c88cb1e4003667b7e078ee4084887cf839bbdf831", size = 25556, upload-time = "2026-05-25T12:09:00.292Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, +] + +[[package]] +name = "requests" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, +] + +[[package]] +name = "requirements-parser" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/96/fb6dbfebb524d5601d359a47c78fe7ba1eef90fc4096404aa60c9a906fbb/requirements_parser-0.13.0.tar.gz", hash = "sha256:0843119ca2cb2331de4eb31b10d70462e39ace698fd660a915c247d2301a4418", size = 22630, upload-time = "2025-05-21T13:42:05.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/60/50fbb6ffb35f733654466f1a90d162bcbea358adc3b0871339254fbc37b2/requirements_parser-0.13.0-py3-none-any.whl", hash = "sha256:2b3173faecf19ec5501971b7222d38f04cb45bb9d87d0ad629ca71e2e62ded14", size = 14782, upload-time = "2025-05-21T13:42:04.007Z" }, +] + +[[package]] +name = "responses" +version = "0.25.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "restructuredtext-lint" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/e6/eefcad2228f4124f17e01064428fbcd0ade06a274f3063ce3a126a569d6b/restructuredtext_lint-2.0.2.tar.gz", hash = "sha256:dd25209b9e0b726929d8306339faf723734a3137db382bcf27294fa18a6bc52b", size = 17494, upload-time = "2025-11-23T08:05:18.585Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/63/ac52b32b33ae62f2076ed5c4f6b00e065e3ccbb2063e9a2e813b2bfc95bf/restructuredtext_lint-2.0.2-py3-none-any.whl", hash = "sha256:374c0d3e7e0867b2335146a145343ac619400623716b211b9a010c94426bbed7", size = 14198, upload-time = "2025-11-23T08:05:23.267Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, +] + +[[package]] +name = "ruyaml" +version = "0.91.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distro" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/75/abbc7eab08bad7f47887a0555d3ac9e3947f89d2416678c08e025e449fdc/ruyaml-0.91.0.tar.gz", hash = "sha256:6ce9de9f4d082d696d3bde264664d1bcdca8f5a9dff9d1a1f1a127969ab871ab", size = 239075, upload-time = "2021-12-07T16:19:58.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/9a/16ca152a04b231c179c626de40af1d5d0bc2bc57bc875c397706016ddb2b/ruyaml-0.91.0-py3-none-any.whl", hash = "sha256:50e0ee3389c77ad340e209472e0effd41ae0275246df00cdad0a067532171755", size = 108906, upload-time = "2021-12-07T16:19:56.798Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/ff/f75651350db3cf2ef767371307eb163f3cc1ac03e16fdf3ac347607f7edb/setuptools-80.10.1.tar.gz", hash = "sha256:bf2e513eb8144c3298a3bd28ab1a5edb739131ec5c22e045ff93cd7f5319703a", size = 1229650, upload-time = "2026-01-21T09:42:03.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/76/f963c61683a39084aa575f98089253e1e852a4417cb8a3a8a422923a5246/setuptools-80.10.1-py3-none-any.whl", hash = "sha256:fc30c51cbcb8199a219c12cc9c281b5925a4978d212f84229c909636d9f6984e", size = 1099859, upload-time = "2026-01-21T09:42:00.688Z" }, +] + +[[package]] +name = "setuptools-scm" +version = "10.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "vcs-versioning" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/b1/d0b97ffd2856a7d19c63024a89fb84813cb9d2ed7fa8fdbedf9e2f13a9ab/setuptools_scm-10.2.1.tar.gz", hash = "sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9", size = 154237, upload-time = "2026-07-21T08:08:04.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/b4/02ac1d9833b882c87b3a4e82703e70eac4ab33d0d15fb123e60bb01f3bc1/setuptools_scm-10.2.1-py3-none-any.whl", hash = "sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580", size = 29078, upload-time = "2026-07-21T08:08:03.293Z" }, +] + +[[package]] +name = "shellcheck-py" +version = "0.11.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/55/455b097417b3df3d330eff029c72c32f08b25739e3010acb30ad06d268ef/shellcheck_py-0.11.0.1.tar.gz", hash = "sha256:5c620c88901e8f1d3be5934b31ea99e3310065e1245253741eafd0a275c8c9cc", size = 3139, upload-time = "2025-08-09T17:53:42.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/27/d75b03e5458cefdb6d3b674566cd20476c3e4d3fe6cc9d68b7e3b854b296/shellcheck_py-0.11.0.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:b6a3fee28efda2e16e38d6e6d59faf7224300256456639727370d404730849e8", size = 6774472, upload-time = "2025-08-09T17:53:34.573Z" }, + { url = "https://files.pythonhosted.org/packages/61/ac/2a84c37171c0cf5a10ea4b0a27d43eb0a1d29bd98b49c2c5ffe17ad24bbe/shellcheck_py-0.11.0.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:6b88d0a244c82ed07e06a53e444da841f69330ca59ae15d4a66c391655dae7a0", size = 11381835, upload-time = "2025-08-09T17:53:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/96/55/250e0e3367613a5c22bd82e33b16b889287d81ab0f7dda67e6514a4cccf4/shellcheck_py-0.11.0.1-py2.py3-none-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b274df81de5b000ff78db433e7328b87e52e3c38481c60f8e488c3095beef05", size = 3800600, upload-time = "2025-08-09T17:53:38.643Z" }, + { url = "https://files.pythonhosted.org/packages/15/5b/bb14c0a7474463b1aa3c09e866cb172dffc66ed2993b7ea8f1db581e86ee/shellcheck_py-0.11.0.1-py2.py3-none-win_amd64.whl", hash = "sha256:784156289ecb17e91c692cd783ab5152333309588cabb10032a047331c63e759", size = 8027541, upload-time = "2025-08-09T17:53:40.889Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "shfmt-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/d5/c2ad5c6593a34da7344cf39bde65763e8cda752589074ba1619e55b317ad/shfmt_py-4.0.0.tar.gz", hash = "sha256:1e5fdacf40aabaa77a97639d52a6220df0893b46658d82b7f136f4e66e2b2fb0", size = 11947, upload-time = "2026-05-13T09:25:50.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/1d/8f72824e2a0e06dc0bc2687baacaba0573be7d2e93c01d1e895fddd8c13e/shfmt_py-4.0.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:75a4919a03fb3bcff9795e3cc7b971e37e74905654d2f11605001cab42e5f92f", size = 1343695, upload-time = "2026-05-13T09:25:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/a8/82/9564a2c2a76fbec94db1b3a3c37a9a1d00e7eafca2cdd2e0d19082618d7e/shfmt_py-4.0.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:bb3d236163ff39c7790953e069938caf247e7646399f7a059f00f65d4e6916d6", size = 1237947, upload-time = "2026-05-13T09:25:44.767Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a9/6fce944efa530db941edd11388d70dc7384aaf12169a3b0847b6a6c987b0/shfmt_py-4.0.0-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:4701336c3cb5f3959a5e85481b14f02054ea094b3c666f3d04649bbe10de3c25", size = 1218771, upload-time = "2026-05-13T09:25:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/64/43/e3965a25bb39555f2791c6860214f62b6f976f9ac7e9786073364bcdd9a6/shfmt_py-4.0.0-py2.py3-none-manylinux2014_x86_64.whl", hash = "sha256:e57877abe0177a9da7bbb5390fe7e96aa19b00958189a025634039aef8834d44", size = 1350939, upload-time = "2026-05-13T09:25:47.584Z" }, + { url = "https://files.pythonhosted.org/packages/95/20/db2430d9262d2cffadcad2b330441e13031f1ab849ec069659edb7f23257/shfmt_py-4.0.0-py2.py3-none-win_amd64.whl", hash = "sha256:bd4f3d36264d4ba8b014ff73e5e702aaa2345845c021f563480128de3705135b", size = 1427721, upload-time = "2026-05-13T09:25:48.865Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, +] + +[[package]] +name = "sphinx-click" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/ed/a9767cd1b8b7fbdf260a89d5c8c86e20e3536b9878579e5ab7965a291e55/sphinx_click-6.2.0.tar.gz", hash = "sha256:fc78b4154a4e5159462e36de55b8643747da6cda86b3b52a8bb62289e603776c", size = 27035, upload-time = "2025-12-04T19:33:05.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/bd/cb244695f67f77b0a36200ce1670fc42a6fe2770847e870daab99cc2b177/sphinx_click-6.2.0-py3-none-any.whl", hash = "sha256:1fb1851cb4f2c286d43cbcd57f55db6ef5a8d208bfc3370f19adde232e5803d7", size = 8939, upload-time = "2025-12-04T19:33:04.037Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + +[[package]] +name = "sphinx-inline-tabs" +version = "2025.12.21.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/6a/f39bde46a79b80a9983233d99b773bd24b468bdd9c1e87acb46ff69af441/sphinx_inline_tabs-2025.12.21.14.tar.gz", hash = "sha256:c71a75800326e613fb4e410eed92a0934214741326aca9897c18018b9f968cb6", size = 45572, upload-time = "2025-12-21T13:30:51.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/2b/e64e7de34663cff1df029ba4f05a86124315bd9eba3d3b78e64904bea7e0/sphinx_inline_tabs-2025.12.21.14-py3-none-any.whl", hash = "sha256:e685c782b58d4e01490bcc4e2367cf7135ec28e7283a05e89095394e4ca6e81a", size = 7082, upload-time = "2025-12-21T13:30:50.142Z" }, +] + +[[package]] +name = "sphinx-lint" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "polib" }, + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/19/9258497fee6e2a0bdb93e8ecea6ef6864afb5d83e996a1606a853f96c658/sphinx_lint-1.0.2.tar.gz", hash = "sha256:4e7fc12f44f750b0006eaad237d7db9b1d8aba92adda9c838af891654b371d35", size = 36870, upload-time = "2025-11-19T08:28:12.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/62/f29a2988ff706ac01d3c63d0b4cc4ed62d2c83b447916e0317790ca156cf/sphinx_lint-1.0.2-py3-none-any.whl", hash = "sha256:edcd0fa4d916386c5a3ef7ef0f5136f0bb4a15feefc83c1068ba15bc16eec652", size = 20670, upload-time = "2025-11-19T08:28:10.656Z" }, +] + +[[package]] +name = "sphinx-pyproject" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dom-toml" }, + { name = "domdf-python-tools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/97/aa8cec3da3e78f2c396b63332e2fe92fe43f7ff2ad19b3998735f28b0a7f/sphinx_pyproject-0.3.0.tar.gz", hash = "sha256:efc4ee9d96f579c4e4ed1ac273868c64565e88c8e37fe6ec2dc59fbcd57684ab", size = 7695, upload-time = "2023-08-18T21:43:45.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/d5/89cb47c6399fd57ca451af15361499813c5d53e588cb6e00d89411ce724f/sphinx_pyproject-0.3.0-py3-none-any.whl", hash = "sha256:3aca968919f5ecd390f96874c3f64a43c9c7fcfdc2fd4191a781ad9228501b52", size = 23076, upload-time = "2023-08-18T21:43:43.808Z" }, +] + +[[package]] +name = "sphinx-substitution-extensions" +version = "2026.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "docutils" }, + { name = "myst-parser" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/d9/d7bd4d3a396b05fa441b46e70d6b50971b219a521ffef5cc1f17f2aff7ed/sphinx_substitution_extensions-2026.8.5.tar.gz", hash = "sha256:c64c0c3cd4d2d4c59d761252876b0fd8367e1023bc54e333bf06b1a434965503", size = 42300, upload-time = "2026-08-05T12:21:17.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/2a/8d3315e55c53c56db1a178e78a6081d65fde7607f6ae8f9b81e91cf5e8d9/sphinx_substitution_extensions-2026.8.5-py3-none-any.whl", hash = "sha256:2eda9c056ccf760cf227c0f76cfc6e7875c89fa8edacf64e7518fcfbd029c623", size = 11773, upload-time = "2026-08-05T12:21:16.21Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sphinxcontrib-spelling" +version = "8.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyenchant" }, + { name = "requests" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/cd/fa8039cedce6295644ff5f03367742d21e7922da426e8c666b7f5a213682/sphinxcontrib_spelling-8.0.2.tar.gz", hash = "sha256:afbc7b8e93721ab88f12bdd39d848b92017b3763b9ed6226b4b0e54b06664fea", size = 30955, upload-time = "2025-11-28T15:31:50.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c5/bcd32aa919c9e1652cca5bed6478202656a320c407793b547f8c16c179e3/sphinxcontrib_spelling-8.0.2-py3-none-any.whl", hash = "sha256:db8b3b2945683d49e87a8a5133d2b8ed4206cb593038b986ca8686a485f9980d", size = 14587, upload-time = "2025-11-28T15:31:48.957Z" }, +] + +[[package]] +name = "sphinxcontrib-towncrier" +version = "0.5.0a0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, + { name = "towncrier" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/fe/72ed57093e28af10595c50839b183c5fdf0952482e9ef0ca6eb90eb85c5d/sphinxcontrib_towncrier-0.5.0a0.tar.gz", hash = "sha256:294e69df6e275e7a86df7ea6a927cc7c28c2c370a884cd5c45de6ec989858f27", size = 62453, upload-time = "2025-02-28T01:59:16.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/5c/f7e39f243636a5e1894f2f5a72579977bf3968922afdb75175ee45062066/sphinxcontrib_towncrier-0.5.0a0-py3-none-any.whl", hash = "sha256:11d130c3ad5e4649821d543c4ea7ab64bbe78df4d859ef94f4298e7845dc0f59", size = 12609, upload-time = "2025-02-28T01:59:15.178Z" }, +] + +[[package]] +name = "stevedore" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, +] + +[[package]] +name = "strict-kwargs" +version = "2026.7.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ty" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/ea/5be0b5ba196632ae08842b7e7a45c2baa4ad14be3aa0c410e9ffede761d2/strict_kwargs-2026.7.24-py3-none-macosx_11_0_arm64.whl", hash = "sha256:94a8679e751918df94239ae4513ce99d89653cbdf55eb7d9181037ded0a79562", size = 2897038, upload-time = "2026-07-24T09:33:29.353Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/5e23899b7ff873217aead4d0f6a89fa50b1fd7c0cac04c93d713ae9ba4a8/strict_kwargs-2026.7.24-py3-none-manylinux_2_39_x86_64.whl", hash = "sha256:b788a347f66f5ee1ebc424632a2bce223ed0162da97a12327af75e0438df2836", size = 3084814, upload-time = "2026-07-24T09:33:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/43/ad/ef1e18303b9684e81dc9ffdb4ec3e8bb5065d5b8ec1b1da6cae0451341cc/strict_kwargs-2026.7.24-py3-none-win_amd64.whl", hash = "sha256:b727e83eff48ef0c6e14618f1ce04e70e4b9b8c7f0ee5f2a546bf8d30e77c729", size = 2799438, upload-time = "2026-07-24T09:33:32.471Z" }, +] + +[[package]] +name = "sybil" +version = "9.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/46/bae21847b8d761ddd6ede1811d32818342dbd482c32a2a5805c28d9b2f18/sybil-9.3.0.tar.gz", hash = "sha256:847d1d17b8a857c4bb3f8471b4a57b8affa939a60fbf507e70aa72ad79097c05", size = 89078, upload-time = "2025-12-02T09:29:09.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/08/cd3cf2a82570748cfb3142e795044197deff81ad3b70a0b9a9c22331e70a/sybil-9.3.0-py3-none-any.whl", hash = "sha256:0b108b980ab9fac774953042b07fcb5858aa19a38404d0cb42c30c93423ac0c1", size = 39286, upload-time = "2025-12-02T09:29:08.417Z" }, +] + +[[package]] +name = "sybil-extras" +version = "2026.7.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "markdown-it-py" }, + { name = "myst-parser" }, + { name = "sybil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/3c/27b1055b5afb809f055c68ba5858a4a498757f54f90827e16918ce53f39d/sybil_extras-2026.7.19.tar.gz", hash = "sha256:4ee756a2da38287a957bde7edbf295951cdd65407ca4344a1726dfff3c1d64ba", size = 118325, upload-time = "2026-07-19T13:47:44.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/1d/d09751fe1bc2d9029c5123ce4c5fb11ead2d09c8f227a8f4658ebe3e7afa/sybil_extras-2026.7.19-py3-none-any.whl", hash = "sha256:794cb004f1f8d2b7e32b7ca1af455972d382053c1c0d03ac2da8c4089616c9fb", size = 89540, upload-time = "2026-07-19T13:47:43.107Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version < '3.15' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "fsspec", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "jinja2", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "networkx", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "setuptools", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "sympy", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", upload-time = "2026-07-08T12:26:28Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", upload-time = "2026-07-08T12:26:33Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15' and sys_platform != 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "fsspec", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "jinja2", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "networkx", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "setuptools", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "sympy", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:dec241fef3984c0d1edadd1f58708e218d4eae881ceef7bc10cf9964d41b68b9", upload-time = "2026-07-08T19:30:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ca021f9eb2f8345c83fa03e3a04587308afb8df71bd472670b3ece00df58621c", upload-time = "2026-07-08T19:30:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d20fa53ee744502fa4c69818a720b05ca0d37abd055d4f6e66cae155114bc691", upload-time = "2026-07-08T19:30:45Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:e2e5134decf00e218da62318f3dc5df156231d367871918e91eba95ab0ad43ab", upload-time = "2026-07-08T19:30:58Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:991cc14b39e751122c01f017be6448533989868731cb5eecd1006893d26787c2", upload-time = "2026-07-08T19:31:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7b8d26e29bceafbdaa8d63bfe7612f23875b5af2cc07e13f809c3ed890bbe1d8", upload-time = "2026-07-08T19:31:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b222c15a0fc2ce207d1c1a59700b46c8fa6748df1f447ad11e5c870dde0933d9", upload-time = "2026-07-08T19:31:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:a43376bd094124ef626bfdd3d4c2c62eacb0b5ddc99776f4a32d4fd16f1f3420", upload-time = "2026-07-08T19:31:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5002ca81af00ae69b57540f615b58b8ae922b6d4848176b366a52bd2196e6", upload-time = "2026-07-08T19:32:00Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:1a3a35229fdc13446b4eab50e7fcf9399ff941e89a3b761497786297a5d8dde5", upload-time = "2026-07-08T19:32:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:8e109528e6bab044815daebaf71770fbaace3a66ef1c816cb55c875350f78a60", upload-time = "2026-07-08T19:32:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:222a6681467cc7f6f05cd3068dfbc603def3a1e46d1d4620c1c8cdf6178bd563", upload-time = "2026-07-08T19:32:44Z" }, +] + +[[package]] +name = "torchvision" +version = "0.28.0" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version < '3.15' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "pillow", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.13.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b", upload-time = "2026-07-08T12:26:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49", upload-time = "2026-07-08T12:26:40Z" }, +] + +[[package]] +name = "torchvision" +version = "0.28.0+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15' and sys_platform != 'darwin'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "pillow", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a1a76c8decb1d7bbedd3588bccc90fb269944b7321a773db181735b42115422", upload-time = "2026-07-08T12:26:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:1aa741ae0eb8668b6287dd667548e2dd10179c828db68bfdee1519763b9c5b99", upload-time = "2026-07-08T12:26:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:d2a0171faa211b506c4dcf3a036942a41077c5d2d3d94883dafbda7b8624a3eb", upload-time = "2026-07-08T12:26:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:8d8b98608779c770ede5e20609772453ebc7487ebb8697445d1856466c542f45", upload-time = "2026-07-08T12:26:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:870d0d42f2eb80f4870cd35e51eea52f596a408a671b28136f06a808846f24c5", upload-time = "2026-07-08T12:26:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:82dbffb63d61cd43d9c7a311588e665aa2b21173a05f852b4d384d6782fd88ef", upload-time = "2026-07-08T12:26:39Z" }, +] + +[[package]] +name = "towncrier" +version = "25.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/06/8ba22ec32c74ac1be3baa26116e3c28bc0e76a5387476921d20b6fdade11/towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513", size = 65101, upload-time = "2025-08-30T11:41:53.644Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2026.1.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, +] + +[[package]] +name = "ty" +version = "0.0.70" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ed/38a8ab52f1d7c3ed701442a31b23ba774cbc5d6909f2c00da9e1f3c590f9/ty-0.0.70.tar.gz", hash = "sha256:a01bebc128b4081c16002965d906fccb21323d69bb709b9108c1f2406bcffced", size = 6601156, upload-time = "2026-08-10T23:20:26.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/c5/ddd6cc5657fd3da85591b264f4dabb1ce5e5b535e73fd5174991c294978b/ty-0.0.70-py3-none-linux_armv6l.whl", hash = "sha256:4fb2d2e55e2160c07152361be2e1a26fdd4f6261055731317d5972daf1749935", size = 12537215, upload-time = "2026-08-10T23:19:43.512Z" }, + { url = "https://files.pythonhosted.org/packages/8b/8a/acc9b34331cde81e0d63cda92d4db9e4042def5b1efa7d47431ff1d30d17/ty-0.0.70-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9b04d4c21cb029501c05598ca21e9c0829c0b2e35a7ceba1e5b78014c1e8104d", size = 12149070, upload-time = "2026-08-10T23:19:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cd/51ac2708f4d077058a0bfeefa40d630883c5ec7a82fd2c15c86519140235/ty-0.0.70-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c605ebf2643f5e64ec4bcb269640a1aa85966d29fa888fb746039c28570369b2", size = 11625729, upload-time = "2026-08-10T23:19:48.535Z" }, + { url = "https://files.pythonhosted.org/packages/b7/99/afc4fe7e630100dc782ff0cdc8c59c01acfb05299551ff0ef49c93814320/ty-0.0.70-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c38ea76e12909c29e18fcd295ff223b63f3701c1d1c34db2cdc9736340b9de2", size = 12204810, upload-time = "2026-08-10T23:19:51.845Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/8cabc3c8ad4c3a02e585ecff12a71ff8e5881f8a00ee71745fd765201da5/ty-0.0.70-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1ee6bf4edaabf7bd0307f0c0f9ddc2204df0390820fe89f6a9de65f07722aba", size = 12308114, upload-time = "2026-08-10T23:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/44/7e/bb4e552ecd68bb490bae9368ef323e3d0c79ef85a811857139a2d0f59ddb/ty-0.0.70-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5eef3e11d7d6b800ef66da0cce8ea24f8be804d1ebf359683426cfe848729bf8", size = 13072240, upload-time = "2026-08-10T23:19:56.872Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c4/1861e1d554e5b6e0d11b3a9f36d40c372253f44a3c4e56ad4bf840f2801e/ty-0.0.70-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95e4ae7c2599197c9d89652e49ca344ab224f7d12376e6ad8beb0587e8ff83f", size = 13497678, upload-time = "2026-08-10T23:19:59.191Z" }, + { url = "https://files.pythonhosted.org/packages/77/22/3b22442133e8f641485e59e463a070a31184db0d6456851f871db8f59763/ty-0.0.70-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06aca758d1e0016c0a1f57fe9d8de7a21ff83f692306f747a0d97f32de24e27f", size = 13232510, upload-time = "2026-08-10T23:20:01.456Z" }, + { url = "https://files.pythonhosted.org/packages/84/b8/911f1e6885b5485b6e1d29aaab19ce5e6deeb3e931b6ad82296da4f22051/ty-0.0.70-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d81825524f1b57ecbcb5fce7d61fb159cb4837a6167a4569309c9fa7fc15a77d", size = 12817128, upload-time = "2026-08-10T23:20:04.275Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a6/9affc3ca11c32d75b348a66144ab83335fa7fa7100b1581356725e15a668/ty-0.0.70-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3287dfb09f7320ef28f114f5f9aae5f697b7b1a6ee37fb2a4a3be94481c1b4f8", size = 13089208, upload-time = "2026-08-10T23:20:06.574Z" }, + { url = "https://files.pythonhosted.org/packages/f0/cb/4776108ea08ec4013b71375b101be9c8a632967cd10f24abbdd4664d66f4/ty-0.0.70-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9fb1877f6401cdaac4db46c5bf762f327482ba5f891420ea462ebd15d0de4185", size = 12148890, upload-time = "2026-08-10T23:20:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/814e71f698d9231ef6412bd2c1499d81c6f86f66c2764e952c991e2d6da5/ty-0.0.70-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e78f4997dbb0db2d2270210eb3694466206e5b3a11985e24148770e702158a25", size = 12327084, upload-time = "2026-08-10T23:20:11.023Z" }, + { url = "https://files.pythonhosted.org/packages/3b/69/9f34bba534c5d1ace391d300ead4f1971f4348c5459e66dc466cfd60661c/ty-0.0.70-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cf758d3b2dad910c9b1d22d2d62fea894b5bc9acd3c00365e8a73ace6090a452", size = 12604372, upload-time = "2026-08-10T23:20:13.366Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ff/abb34674517b29a8e2489a39c4e01930fcf6aab993a7d6b33aa97f119117/ty-0.0.70-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0d338761617279a4fb6a83e7fad7e21126394637b8c45e13e196b2bb675f39b2", size = 12917800, upload-time = "2026-08-10T23:20:15.841Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1a/f8289572f4fad5cb16c883b94638548166bc78cf3eb569f0cd9199eb10a0/ty-0.0.70-py3-none-win32.whl", hash = "sha256:a45642cf09dde91f0a3ce9b9e6fffda9779ff69d73f7347c18acf4fb45007c07", size = 11921482, upload-time = "2026-08-10T23:20:18.244Z" }, + { url = "https://files.pythonhosted.org/packages/17/ae/8739d7618b4670c3ee4f52d641d53be87928bd121d8bda9c0e3450500b75/ty-0.0.70-py3-none-win_amd64.whl", hash = "sha256:33e7941a926cf39b82553911a59a6ed68ec98c3d3d5a415df633f4d1cd051e6c", size = 12986994, upload-time = "2026-08-10T23:20:20.53Z" }, + { url = "https://files.pythonhosted.org/packages/f1/44/2bc3301ba4356ad8866daac9f8cfb3687953e52d076ea204f113b9304c42/ty-0.0.70-py3-none-win_arm64.whl", hash = "sha256:0d380f735d52b1d4b773193f8f5c58c065725eab1ebb0008d38a7b830de14f47", size = 12301082, upload-time = "2026-08-10T23:20:23.816Z" }, +] + +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260724" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/6f/a28f44bcd56bebed42b028a2894c79853e2f5e6b5279e633cb3f287a05e7/types_pyyaml-6.0.12.20260724.tar.gz", hash = "sha256:3c1ce1bb73cd5ec02e90390c2b1f00e810d241d8825fd73ff359696839271b6b", size = 17893, upload-time = "2026-07-24T04:58:43.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/42/0337fefc615e20ee55d1c8f71b774a9b2b734a04669139c20753b27a2a3a/types_pyyaml-6.0.12.20260724-py3-none-any.whl", hash = "sha256:d57db930a4b2efbc57cf430ec8882765d246929432fa253092f383902329a453", size = 20312, upload-time = "2026-07-24T04:58:42.486Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "vale" +version = "3.13.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/0d/6ebd7d020135888cc4d35290737871986ceabf176ba5e44827294429283a/vale-3.13.0.0.tar.gz", hash = "sha256:9c2482ecab515e58aa8d7e1a09f3d44ed674a07502786e22ff60c9d4fdc8493b", size = 5340, upload-time = "2025-10-28T13:20:41.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/05/92b9e4d3e3cb424d2a1aa6e070ee838f15f6739a90b06964156a24fe49ce/vale-3.13.0.0-py3-none-any.whl", hash = "sha256:b565197a5f6e430af7ccc59204e75c6067bbd091a0073d700efdd0fba21873ed", size = 5944, upload-time = "2025-10-28T13:20:40.35Z" }, +] + +[[package]] +name = "vcs-versioning" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/4c53c4c161d484e74685fd03768f8874c0beb96c549a58011d3e456f61c9/vcs_versioning-2.1.2.tar.gz", hash = "sha256:7282122aee5de69d520a9dbe56ebac705a0be7e2cc9b1b97dde4333c45fe782c", size = 131716, upload-time = "2026-06-23T21:36:29.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/62/6fe8d9f670980e9e1c879eed5653cd24b6015a892873c206b4ae3fd35526/vcs_versioning-2.1.2-py3-none-any.whl", hash = "sha256:d559440f0cb46b6672e65492c34eadd45a736d61d5b28d43ae1ad37bc4a9f06f", size = 107212, upload-time = "2026-06-23T21:36:27.881Z" }, +] + +[[package]] +name = "vulture" +version = "2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/3e/4d08c5903b2c0c70cad583c170cc4a663fc6a61e2ad00b711fcda61358cd/vulture-2.16.tar.gz", hash = "sha256:f8d9f6e2af03011664a3c6c240c9765b3f392917d3135fddca6d6a68d359f717", size = 52680, upload-time = "2026-03-25T14:41:27.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/f935130312330614811dae2ea9df3f395f6d63889eb6c2e68c14507152ee/vulture-2.16-py3-none-any.whl", hash = "sha256:6e0f1c312cef1c87856957e5c2ca9608834a7c794c2180477f30bf0e4cc58eee", size = 26993, upload-time = "2026-03-25T14:41:26.21Z" }, +] + +[[package]] +name = "vws-auth-tools" +version = "2024.7.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/17/421ff3a46cee7d952e3da3126160fb75ab7cb54e3fbbfa718a7ee9e120d4/vws_auth_tools-2024.7.12.tar.gz", hash = "sha256:e3949606f2366053ea97883992f8ecaf95030ea33f1b3cf769f99f9d43c0914b", size = 19097, upload-time = "2024-07-12T16:59:31.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/34/d6c791bffdc3cb2e920468d255d0fa23366cb8415c1ba3db26127cc1c789/vws_auth_tools-2024.7.12-py2.py3-none-any.whl", hash = "sha256:673bb0be98e2112a008f3146ab24a0276dc26de8c43cb40546d9a54821cb9e48", size = 5327, upload-time = "2024-07-12T16:59:30.287Z" }, +] + +[[package]] +name = "vws-cli" +source = { editable = "." } +dependencies = [ + { name = "beartype" }, + { name = "click" }, + { name = "pyyaml" }, + { name = "vws-python" }, +] + +[package.optional-dependencies] +dev = [ + { name = "actionlint-py" }, + { name = "check-manifest" }, + { name = "deptry" }, + { name = "doc8" }, + { name = "doccmd" }, + { name = "freezegun" }, + { name = "furo" }, + { name = "hadolint-bin", marker = "sys_platform != 'win32'" }, + { name = "interrogate" }, + { name = "mypy", extra = ["faster-cache"] }, + { name = "mypy-strict-kwargs" }, + { name = "no-defaults" }, + { name = "prek" }, + { name = "pydocstringformatter" }, + { name = "pylint", extra = ["spelling"] }, + { name = "pyproject-fmt" }, + { name = "pyrefly" }, + { name = "pyright" }, + { name = "pyroma" }, + { name = "pytest" }, + { name = "pytest-beartype-tests" }, + { name = "pytest-cov" }, + { name = "pytest-regressions" }, + { name = "ruff" }, + { name = "setuptools-scm" }, + { name = "shellcheck-py" }, + { name = "shfmt-py" }, + { name = "sphinx" }, + { name = "sphinx-click" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-inline-tabs" }, + { name = "sphinx-lint" }, + { name = "sphinx-pyproject" }, + { name = "sphinx-substitution-extensions" }, + { name = "sphinxcontrib-spelling" }, + { name = "sphinxcontrib-towncrier" }, + { name = "strict-kwargs" }, + { name = "torch", version = "2.13.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "torchvision", version = "0.28.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.15' and sys_platform == 'darwin'" }, + { name = "torchvision", version = "0.28.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" }, + { name = "towncrier" }, + { name = "ty" }, + { name = "types-pyyaml" }, + { name = "vale" }, + { name = "vulture" }, + { name = "vws-python-mock" }, + { name = "vws-test-fixtures" }, + { name = "yamlfix" }, + { name = "zizmor" }, +] +release = [ + { name = "check-wheel-contents" }, + { name = "homebrew-pypi-poet" }, + { name = "towncrier" }, +] + +[package.metadata] +requires-dist = [ + { name = "actionlint-py", marker = "extra == 'dev'", specifier = "==1.7.12.24" }, + { name = "beartype", specifier = "==0.22.9" }, + { name = "check-manifest", marker = "extra == 'dev'", specifier = "==0.51" }, + { name = "check-wheel-contents", marker = "extra == 'release'", specifier = "==0.6.3" }, + { name = "click", specifier = "==8.4.2" }, + { name = "deptry", marker = "extra == 'dev'", specifier = "==0.25.1" }, + { name = "doc8", marker = "extra == 'dev'", specifier = "==2.0.0" }, + { name = "doccmd", marker = "extra == 'dev'", specifier = "==2026.7.19" }, + { name = "freezegun", marker = "extra == 'dev'", specifier = "==1.5.5" }, + { name = "furo", marker = "extra == 'dev'", specifier = "==2025.12.19" }, + { name = "hadolint-bin", marker = "sys_platform != 'win32' and extra == 'dev'", specifier = "==2.15.1" }, + { name = "homebrew-pypi-poet", marker = "extra == 'release'", specifier = "==0.10.0" }, + { name = "interrogate", marker = "extra == 'dev'", specifier = "==1.7.0" }, + { name = "mypy", extras = ["faster-cache"], marker = "extra == 'dev'", specifier = "==2.3.0" }, + { name = "mypy-strict-kwargs", marker = "extra == 'dev'", specifier = "==2026.7.19.1" }, + { name = "no-defaults", marker = "extra == 'dev'", specifier = "==2.1.0" }, + { name = "prek", marker = "extra == 'dev'", specifier = "==0.4.13" }, + { name = "pydocstringformatter", marker = "extra == 'dev'", specifier = "==1.0.0" }, + { name = "pylint", extras = ["spelling"], marker = "extra == 'dev'", specifier = "==4.0.7" }, + { name = "pyproject-fmt", marker = "extra == 'dev'", specifier = "==2.27.0" }, + { name = "pyrefly", marker = "extra == 'dev'", specifier = "==1.2.0" }, + { name = "pyright", marker = "extra == 'dev'", specifier = "==1.1.411" }, + { name = "pyroma", marker = "extra == 'dev'", specifier = "==5.0.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = "==9.1.1" }, + { name = "pytest-beartype-tests", marker = "extra == 'dev'", specifier = "==2026.4.26" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = "==7.1.0" }, + { name = "pytest-regressions", marker = "extra == 'dev'", specifier = "==2.11.0" }, + { name = "pyyaml", specifier = "==6.0.3" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.2" }, + { name = "setuptools-scm", marker = "extra == 'dev'", specifier = "==10.2.1" }, + { name = "shellcheck-py", marker = "extra == 'dev'", specifier = "==0.11.0.1" }, + { name = "shfmt-py", marker = "extra == 'dev'", specifier = "==4.0.0" }, + { name = "sphinx", marker = "extra == 'dev'", specifier = "==9.1.0" }, + { name = "sphinx-click", marker = "extra == 'dev'", specifier = "==6.2.0" }, + { name = "sphinx-copybutton", marker = "extra == 'dev'", specifier = "==0.5.2" }, + { name = "sphinx-inline-tabs", marker = "extra == 'dev'", specifier = "==2025.12.21.14" }, + { name = "sphinx-lint", marker = "extra == 'dev'", specifier = "==1.0.2" }, + { name = "sphinx-pyproject", marker = "extra == 'dev'", specifier = "==0.3.0" }, + { name = "sphinx-substitution-extensions", marker = "extra == 'dev'", specifier = "==2026.8.5" }, + { name = "sphinxcontrib-spelling", marker = "extra == 'dev'", specifier = "==8.0.2" }, + { name = "sphinxcontrib-towncrier", marker = "extra == 'dev'", specifier = "==0.5.0a0" }, + { name = "strict-kwargs", marker = "extra == 'dev'", specifier = "==2026.7.24" }, + { name = "torch", marker = "extra == 'dev'", specifier = ">=2.5.1", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torchvision", marker = "extra == 'dev'", specifier = ">=0.20.1", index = "https://download.pytorch.org/whl/cpu" }, + { name = "towncrier", marker = "extra == 'dev'", specifier = "==25.8.0" }, + { name = "towncrier", marker = "extra == 'release'", specifier = "==25.8.0" }, + { name = "ty", marker = "extra == 'dev'", specifier = "==0.0.70" }, + { name = "types-pyyaml", marker = "extra == 'dev'", specifier = "==6.0.12.20260724" }, + { name = "vale", marker = "extra == 'dev'", specifier = "==3.13.0.0" }, + { name = "vulture", marker = "extra == 'dev'", specifier = "==2.16" }, + { name = "vws-python", specifier = "==2026.8.14" }, + { name = "vws-python-mock", marker = "extra == 'dev'", specifier = "==2026.8.14" }, + { name = "vws-test-fixtures", marker = "extra == 'dev'", specifier = "==2023.3.5" }, + { name = "yamlfix", marker = "extra == 'dev'", specifier = "==1.19.1" }, + { name = "zizmor", marker = "extra == 'dev'", specifier = "==1.29.0" }, +] +provides-extras = ["dev", "release"] + +[package.metadata.requires-dev] +dev = [] + +[[package]] +name = "vws-python" +version = "2026.8.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "httpx" }, + { name = "requests" }, + { name = "urllib3" }, + { name = "vws-auth-tools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/b65d70abb437082f723fc71f01003c0fa7a5ff4ee0aff990ec9878609a9a/vws_python-2026.8.14.tar.gz", hash = "sha256:bfcfc6196cbecb0e0c8f11fd7c358d53a8453a102078e8482ac6cc4c885239ca", size = 80716, upload-time = "2026-08-14T14:23:32.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/b0/f8b44fd494abaade88eb396c73c729546d1db1bb13792cd79eb363514f71/vws_python-2026.8.14-py3-none-any.whl", hash = "sha256:e80f9fb64f3190eef82bc7a1685fecb6670795a70169c2546088ccb68b135381", size = 47366, upload-time = "2026-08-14T14:23:30.846Z" }, +] + +[[package]] +name = "vws-python-mock" +version = "2026.8.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "flask" }, + { name = "httpx" }, + { name = "numpy" }, + { name = "opencv-contrib-python-headless" }, + { name = "pillow" }, + { name = "pydantic-settings" }, + { name = "pyteenybrisque" }, + { name = "requests" }, + { name = "responses" }, + { name = "respx" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, + { name = "vws-auth-tools" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/4d/4502c5cf6ecedf2121afe7c018f000bae8bc178f2722f4cc1073d14a01e8/vws_python_mock-2026.8.14.tar.gz", hash = "sha256:9a8fdf265cabd8539450066b09f07185a72decfec03cd67b6594e014724d6617", size = 340205, upload-time = "2026-08-14T11:03:14.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/e3/c853170d8e47479523add821f88acdb863f18f73babcd66a67337b74131b/vws_python_mock-2026.8.14-py3-none-any.whl", hash = "sha256:eff8f040bf465a4799cf94072a37807672a647e04ebcc6b314817252c2fdd1d1", size = 99382, upload-time = "2026-08-14T11:03:13.102Z" }, +] + +[[package]] +name = "vws-test-fixtures" +version = "2023.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/f2/db267b21f32539d78aae06add3b31fa1e562a00a5b7d0aa1d0ee18aaadf9/vws-test-fixtures-2023.3.5.tar.gz", hash = "sha256:ba9baafb6fc8cd63338ee9c2b7c70876e6b33c6eb85edf7c4e8e88d9ab25467c", size = 61510, upload-time = "2023-03-05T17:26:04.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/40/e50b6c31637dfb41b13ffc41c799f210aa0fbb56853d32fee8cdfd8d9712/vws_test_fixtures-2023.3.5-py2.py3-none-any.whl", hash = "sha256:7f9f6a6be8e31bdd3ae4f290e7dea6637dc213a2da4c59d0cb6051c2054dee9f", size = 49364, upload-time = "2023-03-05T17:26:02.051Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, +] + +[[package]] +name = "wheel-filename" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/be/726dab762b770d0417e505c58e26d661aac1ec0c831e483cda4817ca2417/wheel_filename-1.4.2.tar.gz", hash = "sha256:87891c465dcbb40b40394a906f01a93214bdd51aa5d25e3a9a59cae62bc298fd", size = 7911, upload-time = "2024-12-01T13:03:16.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/0f/6e97a3bc38cdde32e3ec49f8c0903fe3559ec9ec9db181782f0bb4417717/wheel_filename-1.4.2-py3-none-any.whl", hash = "sha256:3fa599046443d4ca830d06e3d180cd0a675d5871af0a68daa5623318bb4d17e3", size = 6195, upload-time = "2024-12-01T13:03:00.536Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "yamlfix" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "maison" }, + { name = "pydantic" }, + { name = "ruyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/1d/b60d4411ff495de9b7598cc041e29c661e8e2f9d476a8a09bad1f54c1bce/yamlfix-1.19.1.tar.gz", hash = "sha256:05f6add13959637564f278e9237f6e201ff75e061a0a4cb9fc06fa95c3001a22", size = 39483, upload-time = "2025-12-18T09:57:23.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/c7/cba5941b7066f59dbddfe88bdc7154edbe5119bacb3814599997fbc2acac/yamlfix-1.19.1-py3-none-any.whl", hash = "sha256:b885fcf171a2eb59df83c219355bb17dd147675645e2756754372c0bd0b80ea5", size = 28393, upload-time = "2025-12-18T09:57:21.547Z" }, +] + +[[package]] +name = "zizmor" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/f8/f4e3fc0b316d5241b6d6968e8fb702e28446bc7d3c1e2b229f4caa6eacf2/zizmor-1.29.0.tar.gz", hash = "sha256:60e34e83c67064e0036989c7c525d13413e897aa4c4f683f1efb2048cdb28a47", size = 571865, upload-time = "2026-08-01T21:09:19.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/97/667ef4db0ca9225ee402c1b947b5b6f17fd234d72c15a71db150b7695c62/zizmor-1.29.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ea72f84d610643d57f96430c655a3780d0b874e477d32e14eae8e910f6cce1fd", size = 9037504, upload-time = "2026-08-01T21:08:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d4/9fc7deaf75778e7516fa1d6c836377c3cb5d203dedc28899946b6f11ecdb/zizmor-1.29.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5aafe617d7b1e0c0c15d58fdf20495f360f74a791dfa136f76630b4cc06c2a34", size = 8654426, upload-time = "2026-08-01T21:08:59.212Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f6/6db714fb0aa08aeec62eb9d6ad6a443a1f4ed50d4c0b789944ae55fb83e4/zizmor-1.29.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:67644ae8d6d0394204b9a488f7d86f0dd66fe562f4ba85fc53e6105a6bfc7b6a", size = 8918927, upload-time = "2026-08-01T21:09:01.46Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/a12edc0c0c0a0101c54dbb9099ff08f8de2fcb35c36cb706db3deb2c2728/zizmor-1.29.0-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:81e4093fed5c8a41d6ae7bb773085a9d2e6c0b0a0b560d46a9c76d69be0a07ed", size = 8500655, upload-time = "2026-08-01T21:09:03.677Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/dfa67018b76bc4f2f50e265e8cbd1293833d1b1de5f3f02fbbb7487ae9c6/zizmor-1.29.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:587b99c2e1b34575c6c8565c2bfde415ca8bc0310f5589f19bc948c8dea10a20", size = 9351035, upload-time = "2026-08-01T21:09:06.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/1b/93cdd5a06984b394d90001f9778008a21689052904109080a09952626c99/zizmor-1.29.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:061600f23c46f2e400bcdef666c236de7e5c0b07dd6ca046daa001eb1514b909", size = 8941717, upload-time = "2026-08-01T21:09:08.861Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f4/8d9e54405b477bc8e4b56c1a60123fca26c109ea6a762eea104fab32555e/zizmor-1.29.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:332546480be38aca95c149f835e0dcb7679ab5d74618a90c6ccb3fa6b8c7b99d", size = 8468289, upload-time = "2026-08-01T21:09:11.096Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/06d57ca830cc4653369c5aca22cccbf04c8c36ee84a67f351214e556bad8/zizmor-1.29.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a7462b9ab45d72a20ad5ab8193b430df8184c59e2bf46954ddd09496f2f00b45", size = 9446505, upload-time = "2026-08-01T21:09:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/253d9a3538e0ea6f96a3bcd69839c0c5a816a00299620b58a10b5bd1df59/zizmor-1.29.0-py3-none-win32.whl", hash = "sha256:8c759e68cd866375030ca39e19e2de47a056b7be7288c1620e2d5b4c274f631f", size = 7655723, upload-time = "2026-08-01T21:09:15.758Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2d/7919bc23475273ed8038a031fc24bc3f2005c78e608ce8df96746ef0fb98/zizmor-1.29.0-py3-none-win_amd64.whl", hash = "sha256:0fb85948ba5ffc7a8116eee36fe9cfc10167225c97bd2810e3378e66a9fd27c4", size = 8785519, upload-time = "2026-08-01T21:09:17.513Z" }, +] diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 00000000..048ffa79 --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,12 @@ +--- +rules: + unpinned-uses: + disable: true + cache-poisoning: + disable: true + dependabot-cooldown: + disable: true + superfluous-actions: + disable: true + artipacked: + disable: true