diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..9b437c95 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,47 @@ +"area: editor": + - changed-files: + - any-glob-to-any-file: ["src/biscuit/editor/**", "src/biscuit/layout/**"] + +"area: lsp": + - changed-files: + - any-glob-to-any-file: ["src/biscuit/common/lsp/**"] + +"area: git": + - changed-files: + - any-glob-to-any-file: ["src/biscuit/common/git/**", "src/biscuit/views/git/**"] + +"area: ai": + - changed-files: + - any-glob-to-any-file: ["src/biscuit/common/ai/**", "src/biscuit/views/ai/**"] + +"area: debugger": + - changed-files: + - any-glob-to-any-file: ["src/biscuit/common/debugger/**", "src/biscuit/views/debugger/**"] + +"area: terminal": + - changed-files: + - any-glob-to-any-file: ["src/biscuit/common/terminal/**", "src/biscuit/views/terminal/**"] + +"area: extensions": + - changed-files: + - any-glob-to-any-file: ["src/biscuit/extensions/**", "src/biscuit/views/extensions/**"] + +"area: ui": + - changed-files: + - any-glob-to-any-file: ["src/biscuit/common/ui/**", "src/biscuit/views/**", "src/biscuit/layout/**"] + +"area: docs": + - changed-files: + - any-glob-to-any-file: ["docs/**", "*.md"] + +"area: tests": + - changed-files: + - any-glob-to-any-file: ["tests/**"] + +"area: cicd": + - changed-files: + - any-glob-to-any-file: [".github/**"] + +"area: dependencies": + - changed-files: + - any-glob-to-any-file: ["pyproject.toml", "poetry.lock", "uv.lock", "requirements.txt"] diff --git a/.github/workflows/codequality.yml b/.github/workflows/codequality.yml index 7266df61..0e893157 100644 --- a/.github/workflows/codequality.yml +++ b/.github/workflows/codequality.yml @@ -5,44 +5,97 @@ on: branches: ["main"] pull_request: branches: ["main"] + workflow_dispatch: permissions: contents: read jobs: - build: - runs-on: windows-latest + lint: + name: Lint & Code Quality + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - name: Set up Python 3.12 - uses: actions/setup-python@v5 + - name: Set up Python + uses: actions/setup-python@v7 with: - python-version: '3.12' + python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: enable-cache: true - - name: Install dependencies + - name: Install lint tools + run: | + uv pip install --system flake8 ruff + + - name: Ruff Check + run: | + ruff check src tests scripts --select=E9,F63,F7,F82 + + - name: Flake8 Critical Errors + run: | + flake8 src tests scripts --count --select=E9,F63,F7,F82 --show-source --statistics + + - name: Flake8 Style & Complexity Warnings + run: | + flake8 src tests scripts --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + test: + name: Test (${{ matrix.os }}, py${{ matrix.python-version }}) + needs: [lint] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] + include: + - os: macos-latest + python-version: "3.12" + + steps: + - uses: actions/checkout@v7 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install Linux Prerequisites + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y python3-tk xvfb libfontconfig1-dev build-essential + + - name: Install Dependencies run: | uv venv uv pip install scikit-build uv pip install -e . - uv pip install pytest pytest-dotenv pytest-cov flake8 + uv pip install pytest pytest-dotenv pytest-cov pytest-asyncio - - name: Lint with flake8 + - name: Test with pytest (Linux with xvfb) + if: runner.os == 'Linux' run: | - uv run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - uv run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + xvfb-run -a uv run pytest -m "not integration" --cov=biscuit --cov-report=xml --cov-report=term - - name: Test with pytest - run: uv run pytest -m "not integration" --cov=biscuit --cov-report=xml --cov-report=term + - name: Test with pytest (Windows / macOS) + if: runner.os != 'Linux' + run: | + uv run pytest -m "not integration" --cov=biscuit --cov-report=xml --cov-report=term - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 + if: matrix.python-version == '3.12' && matrix.os == 'windows-latest' + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml - fail_ci_if_error: true + fail_ci_if_error: false diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 00000000..c1d34477 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,29 @@ +name: Dependabot Auto-Merge + +on: + pull_request: + branches: ["main"] + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Approve and Enable auto-merge for Dependabot PR + if: steps.metadata.outputs.update-type != 'version-update:semver-major' + run: | + gh pr review --approve "$PR_URL" + gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..890a1578 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,44 @@ +name: Documentation Verification + +on: + push: + branches: ["main"] + paths: + - "docs/**" + - "tests/test_docs.py" + - ".github/workflows/docs.yml" + pull_request: + branches: ["main"] + paths: + - "docs/**" + - "tests/test_docs.py" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate-docs: + name: Validate Mintlify Docs & Assets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install test runner + run: | + uv pip install --system pytest + + - name: Run Docs Integrity Tests + run: | + pytest tests/test_docs.py -v diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index cdbbb180..db9a7cea 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest if: contains(github.event.head_commit.message, 'nightly') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python 3.10 - uses: actions/setup-python@v3 + uses: actions/setup-python@v7 with: python-version: "3.10" @@ -32,7 +32,7 @@ jobs: run: uv run pyinstaller scripts/linux.spec - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: "biscuit-nightly-anylinux" path: "dist/" @@ -41,10 +41,10 @@ jobs: runs-on: windows-latest if: contains(github.event.head_commit.message, 'nightly') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python 3.10 - uses: actions/setup-python@v3 + uses: actions/setup-python@v7 with: python-version: "3.10" @@ -58,7 +58,7 @@ jobs: run: uv run pyinstaller scripts/windows.spec - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: "biscuit-nightly-windows" path: "dist/" diff --git a/.github/workflows/nuitka.yml b/.github/workflows/nuitka.yml index cae40288..9e0429b2 100644 --- a/.github/workflows/nuitka.yml +++ b/.github/workflows/nuitka.yml @@ -11,10 +11,10 @@ jobs: if: contains(github.event.head_commit.message, 'nuitka') steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: "3.10" @@ -31,7 +31,7 @@ jobs: run: echo Yes| uv run python -m nuitka --include-data-dir=biscuit\\res=res --include-package-data=tkextrafont --include-package-data=sv_ttk --include-package-data=pygments --include-package-data=tkinterweb --include-package-data=mistune --follow-import-to=pygments --follow-import-to=tkextrafont --follow-import-to=mistune --follow-import-to=tkinterweb --include-package=tkextrafont --include-package=pygments --include-package=mistune --include-package=tkinterweb --include-module=pygments --include-module=mistune --include-module=tkinterweb --include-module=tkextrafont --follow-imports --standalone --enable-plugin=tk-inter biscuit - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: "biscuit-nuitka" path: "biscuit.dist" diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 00000000..268ebff0 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,19 @@ +name: Pull Request Labeler + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + labeler: + name: Auto-label PR + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..32a4d64c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,138 @@ +name: Release Pipeline + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-python: + name: Build Python Wheel & Sdist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Build packages + run: | + uv build + + - name: Upload Python artifacts + uses: actions/upload-artifact@v7 + with: + name: python-dist + path: dist/* + + build-windows: + name: Build Windows Executable + runs-on: windows-latest + steps: + - uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.10" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install dependencies & PyInstaller + run: | + uv venv + uv pip install -e . + uv pip install pyinstaller + + - name: Build standalone executable + run: | + uv run pyinstaller scripts/windows.spec + + - name: Compress Windows bundle + run: | + Compress-Archive -Path dist\biscuit.exe -DestinationPath dist\biscuit-windows-x86_64.zip + + - name: Upload Windows artifact + uses: actions/upload-artifact@v7 + with: + name: windows-dist + path: dist\biscuit-windows-x86_64.zip + + build-linux: + name: Build Linux Executable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.10" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install system prerequisites + run: | + sudo apt-get update + sudo apt-get install -y python3-dev tcl-dev tk-dev fontconfig libfontconfig1-dev cmake build-essential + + - name: Install dependencies & PyInstaller + run: | + uv venv + uv pip install -e . + uv pip install pyinstaller + + - name: Build standalone executable + run: | + uv run pyinstaller scripts/linux.spec + + - name: Compress Linux bundle + run: | + tar -czvf dist/biscuit-linux-x86_64.tar.gz -C dist biscuit + + - name: Upload Linux artifact + uses: actions/upload-artifact@v7 + with: + name: linux-dist + path: dist/biscuit-linux-x86_64.tar.gz + + publish-release: + name: Publish GitHub Release + needs: [build-python, build-windows, build-linux] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Download all build artifacts + uses: actions/download-artifact@v7 + with: + path: release-artifacts + + - name: Create Release and Upload Assets + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/v') + with: + files: | + release-artifacts/python-dist/* + release-artifacts/windows-dist/* + release-artifacts/linux-dist/* + generate_release_notes: true + draft: false + prerelease: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4123f044..c1c14b58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,12 @@ $ source ./.venv/bin/activate > $ sudo pacman -Sy > $ sudo pacman -S base-devel cmake extra-cmake-modules fontconfig tcl tk > ``` +> +> For Fedora based distributions, +> +> ```bash +> $ sudo dnf install fontconfig fontconfig-devel cmake extra-cmake-modules gcc-c++ python3-devel tcl-devel tk-devel +> ``` Install dependencies using either **Poetry** or **uv**: diff --git a/README.md b/README.md index c0095666..b9328be2 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ pip install biscuit-editor # using pip uv tool install biscuit-editor # using uv ``` +> [!NOTE] +> Ensure Tkinter is installed on your system (`sudo apt install python3-tk` on Ubuntu/Debian, `python3-tkinter` on Fedora). Python 3.10+ is supported (with `legacy-cgi` for 3.13+). + quickly open up a project using **`biscuit path/to/src`** and start editing. see other [installation methods](https://tomlin7.github.io/biscuit/getting-started/installation/) if you'd like to (like pyinstaller). diff --git a/docs/ai-assistant/overview.mdx b/docs/ai-assistant/overview.mdx index dd81c811..94df7918 100644 --- a/docs/ai-assistant/overview.mdx +++ b/docs/ai-assistant/overview.mdx @@ -31,7 +31,7 @@ The AI assistant uses a ReAct loop: |----------|--------| | **Google Gemini** | Gemini 2.5 Pro, 2.5 Flash, 2.0 Pro, 2.0 Flash | | **Anthropic Claude** | Claude 4.5 Opus/Sonnet/Haiku, Claude 4 Opus/Sonnet | -| **MiniMax** | MiniMax M3, M2.7 | +| **MiniMax** | MiniMax M3.1 Flash, M3 | | **Custom** | Add more via extensions | diff --git a/docs/ai-assistant/supported-models.mdx b/docs/ai-assistant/supported-models.mdx index 95719717..6d62d74e 100644 --- a/docs/ai-assistant/supported-models.mdx +++ b/docs/ai-assistant/supported-models.mdx @@ -31,8 +31,13 @@ of supported models. | Model | Best For | |-------|----------| +| MiniMax M3.1 Flash | Default MiniMax model for coding tasks | | MiniMax M3 | General coding tasks | -| MiniMax M2.7 | Fast, efficient assistance | + +MiniMax requests use the Anthropic-compatible API. Flash always uses thinking, +with the API's default `max` effort; M3 uses the API's default of thinking off. +Saved legacy or custom model IDs remain selected and are sent unchanged. Their +availability depends on the API. ## Adding Custom Providers diff --git a/docs/installation.mdx b/docs/installation.mdx index 74500227..445c6b44 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -3,13 +3,17 @@ title: "Installation" description: "Install Biscuit on Windows, Linux, or macOS via pip, uv, or from source." --- -## Prerequisites - -Biscuit requires **Python 3.10 or later** (Python 3.11+ recommended for development). +Biscuit requires **Python 3.10 or later** with Tkinter support (Python 3.11+ recommended). - On Linux, you may need to install additional system packages. See the - [Linux Prerequisites](#linux-prerequisites) section below. + Tkinter is required by Biscuit. Some Python distributions (especially on Linux or minimal Python installs) do not bundle Tkinter by default. + Please install Tkinter in advance: + - **Debian / Ubuntu**: `sudo apt install python3-tk` + - **Fedora / RHEL**: `sudo dnf install python3-tkinter` + - **Arch Linux**: `sudo pacman -S tk` + - **macOS (Homebrew)**: `brew install python-tk` + + On **Python 3.13+**, standard library `cgi` was removed; Biscuit automatically installs and uses `legacy-cgi` for compatibility. ## Install via pip (recommended) @@ -79,6 +83,13 @@ sudo apt install fontconfig libfontconfig1 libfontconfig1-dev cmake cmake-data e pip install scikit-build ``` +On Fedora-based distributions: + +```bash +sudo dnf install fontconfig fontconfig-devel cmake extra-cmake-modules gcc-c++ python3-devel tcl-devel tk-devel +pip install scikit-build +``` + For other distributions, ensure you have `fontconfig`, a C++ compiler, and CMake installed. diff --git a/poetry.lock b/poetry.lock index 690b208e..b5fadccc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. [[package]] name = "aiodns" @@ -204,14 +204,14 @@ files = [ [[package]] name = "anthropic" -version = "0.117.0" +version = "0.117.1" description = "The official Python library for the anthropic API" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "anthropic-0.117.0-py3-none-any.whl", hash = "sha256:451a0a6905f11dff7663d13e4ee5dbf909eb8942b1d049803c7b937a13ac47ec"}, - {file = "anthropic-0.117.0.tar.gz", hash = "sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496"}, + {file = "anthropic-0.117.1-py3-none-any.whl", hash = "sha256:7ab79e2c33c3dbde54c5800c4d535821e1c6d80bdf7de83ae1c0328bae563b76"}, + {file = "anthropic-0.117.1.tar.gz", hash = "sha256:e015a2d5b99fdf0aecac246e24ddb40d7bf93f9fe5b198771fab1559fb0101b9"}, ] [package.dependencies] @@ -225,7 +225,7 @@ sniffio = "*" typing-extensions = ">=4.14,<5" [package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9,<1)"] aws = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] google-cloud = ["google-auth[requests] (>=2,<3)"] @@ -253,6 +253,26 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] +[[package]] +name = "arrow" +version = "1.4.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205"}, + {file = "arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +tzdata = {version = "*", markers = "python_version >= \"3.9\""} + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2025.2)", "simplejson (==3.*)"] + [[package]] name = "async-timeout" version = "5.0.1" @@ -454,6 +474,18 @@ markers = {main = "extra == \"docs\""} [package.extras] extras = ["regex"] +[[package]] +name = "binaryornot" +version = "0.6.0" +description = "Ultra-lightweight pure Python package to check if a file is binary or text." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "binaryornot-0.6.0-py3-none-any.whl", hash = "sha256:900adfd5e1b821255ba7e63139b0396b14c88b9286e74e03b6f51e0200331337"}, + {file = "binaryornot-0.6.0.tar.gz", hash = "sha256:cc8d57cfa71d74ff8c28a7726734d53a851d02fad9e3a5581fb807f989f702f0"}, +] + [[package]] name = "black" version = "24.10.0" @@ -620,6 +652,11 @@ python-versions = ">=3.8" groups = ["main"] markers = "platform_python_implementation != \"CPython\"" files = [ + {file = "brotlicffi-1.2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b13fb476a96f02e477a506423cb5e7bc21e0e3ac4c060c20ba31c44056e38c68"}, + {file = "brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17db36fb581f7b951635cd6849553a95c6f2f53c1a707817d06eae5aeff5f6af"}, + {file = "brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40190192790489a7b054312163d0ce82b07d1b6e706251036898ce1684ef12e9"}, + {file = "brotlicffi-1.2.0.0-cp314-cp314t-win32.whl", hash = "sha256:a8079e8ecc32ecef728036a1d9b7105991ce6a5385cf51ee8c02297c90fb08c2"}, + {file = "brotlicffi-1.2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ca90c4266704ca0a94de8f101b4ec029624273380574e4cf19301acfa46c61a0"}, {file = "brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4"}, {file = "brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7"}, {file = "brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990"}, @@ -961,19 +998,16 @@ files = [ [[package]] name = "click" -version = "8.4.2" +version = "8.5.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "docs"] files = [ - {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, - {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, + {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, + {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, ] -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -985,7 +1019,29 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\" or extra == \"docs\"", dev = "sys_platform == \"win32\""} +markers = {main = "extra == \"docs\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "cookiecutter" +version = "2.7.1" +description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "cookiecutter-2.7.1-py3-none-any.whl", hash = "sha256:cee50defc1eaa7ad0071ee9b9893b746c1b3201b66bf4d3686d0f127c8ed6cf9"}, + {file = "cookiecutter-2.7.1.tar.gz", hash = "sha256:ca7bb7bc8c6ff441fbf53921b5537668000e38d56e28d763a1b73975c66c6138"}, +] + +[package.dependencies] +arrow = "*" +binaryornot = ">=0.4.4" +click = ">=7.0,<9.0.0" +Jinja2 = ">=2.7,<4.0.0" +python-slugify = ">=4.0.0" +pyyaml = ">=5.3.1" +requests = ">=2.23.0" +rich = "*" [[package]] name = "cryptography" @@ -1080,6 +1136,63 @@ webencodings = "*" doc = ["furo", "sphinx"] test = ["pytest", "ruff"] +[[package]] +name = "dap-python" +version = "1.0.1" +description = "Client for Debug Adapter Protocol" +optional = false +python-versions = "<4.0,>=3.12" +groups = ["main"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "dap_python-1.0.1-py3-none-any.whl", hash = "sha256:2888503a8e4b89c6224c2d1a8f8f1303bf749e11d53727f90e5ad4bceb92bece"}, + {file = "dap_python-1.0.1.tar.gz", hash = "sha256:fbe17ae816fd94d627ac4a1522d41b9a8d51f28886e097c016a8b2e435fae6b1"}, +] + +[package.dependencies] +debugpy = ">=1.8.17,<2.0.0" +pydantic = ">=2.12.5,<3.0.0" + +[[package]] +name = "debugpy" +version = "1.8.22" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "debugpy-1.8.22-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735"}, + {file = "debugpy-1.8.22-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee"}, + {file = "debugpy-1.8.22-cp310-cp310-win32.whl", hash = "sha256:56b877b37ed73f0bf53ba7afc394816ff1eb5d701bae24a24ed9e1ea6f7ce34f"}, + {file = "debugpy-1.8.22-cp310-cp310-win_amd64.whl", hash = "sha256:1bd0c6df3c68c0a3f71db8baa3780a953abb65537ec4b3bc6b935ad5b3b3d45c"}, + {file = "debugpy-1.8.22-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:66e4ac3d6e7026e83e7d93d7ee2f51dd4a4e8dff673578d424e60796893e5b2c"}, + {file = "debugpy-1.8.22-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:192b73e8d53bbd60225220c0943627bf249ac93d0ea3090e23c45f3e0ceb6a35"}, + {file = "debugpy-1.8.22-cp311-cp311-win32.whl", hash = "sha256:745e1800ec2961e5660c1a317c0a20e28c0fef4de36c04f17a21c33f2b37a92a"}, + {file = "debugpy-1.8.22-cp311-cp311-win_amd64.whl", hash = "sha256:1e76339d5510bc17e9181dba9577508afcb21aad5728f1a55ef74d7d97d255f3"}, + {file = "debugpy-1.8.22-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ad076f4f66cb8acb79e4384d48b1ea60a0b1957aa0b1112d4887ea3a4df60e0"}, + {file = "debugpy-1.8.22-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8a697acec45dbc70d17fb5d9f4f61989fc294d273c69de487fd10cb35fdd75eb"}, + {file = "debugpy-1.8.22-cp312-cp312-win32.whl", hash = "sha256:12bc7f368182b517cf26c76a2393fff65354e365fa1552b6241e66edff17997b"}, + {file = "debugpy-1.8.22-cp312-cp312-win_amd64.whl", hash = "sha256:371a4ba4a5975eb958393903f3254cf983da7b1c7c178b3f987ee427f42515e3"}, + {file = "debugpy-1.8.22-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:c21dd7e1ec22556bb41dc3bf6b86c603e440ec889c4acb27d7206354b2106c54"}, + {file = "debugpy-1.8.22-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:c1ffb9953708b648ecf6acd2e5ad2c002bf68785ed618197dd4463a2a7226f39"}, + {file = "debugpy-1.8.22-cp313-cp313-win32.whl", hash = "sha256:ba810a66b437e3c43ca0ce0892404f010338286263ca8e5108442d76a9485337"}, + {file = "debugpy-1.8.22-cp313-cp313-win_amd64.whl", hash = "sha256:f49b1d6cecf326b63c06d7f517e4a6642777f758c58cb799e7bc5a6dd74721c2"}, + {file = "debugpy-1.8.22-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d"}, + {file = "debugpy-1.8.22-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9"}, + {file = "debugpy-1.8.22-cp314-cp314-win32.whl", hash = "sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294"}, + {file = "debugpy-1.8.22-cp314-cp314-win_amd64.whl", hash = "sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e"}, + {file = "debugpy-1.8.22-cp315-cp315-macosx_15_0_universal2.whl", hash = "sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07"}, + {file = "debugpy-1.8.22-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5"}, + {file = "debugpy-1.8.22-cp315-cp315-win32.whl", hash = "sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0"}, + {file = "debugpy-1.8.22-cp315-cp315-win_amd64.whl", hash = "sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a"}, + {file = "debugpy-1.8.22-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:cba7b99573ee41f9510c6deeff1cdd0c333748b9dd63d5016413dd57ba9a4593"}, + {file = "debugpy-1.8.22-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:3b7c328cb47b4e2f2b40801936daf8150f88dcb4ffef5080ff119270f0d23626"}, + {file = "debugpy-1.8.22-cp39-cp39-win32.whl", hash = "sha256:ffeeaf4dfe1534375902040df32a4c48f721fd35a4d4d337ac72a44d4176075b"}, + {file = "debugpy-1.8.22-cp39-cp39-win_amd64.whl", hash = "sha256:92fc425308a08f601f3c69afb4719767f9b898276f44b97e6942a713c4c32740"}, + {file = "debugpy-1.8.22-py2.py3-none-any.whl", hash = "sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947"}, + {file = "debugpy-1.8.22.tar.gz", hash = "sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e"}, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -1363,14 +1476,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.52" +version = "3.1.57" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "gitpython-3.1.52-py3-none-any.whl", hash = "sha256:79a36ee1f83523214a3f72d56cf1c4e490d577dc61af77e43dfe5862bd9da01a"}, - {file = "gitpython-3.1.52.tar.gz", hash = "sha256:de0a8ad86274c6e75ae8b37dd055ba68f19818c813108642263227b20775b48e"}, + {file = "gitpython-3.1.57-py3-none-any.whl", hash = "sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf"}, + {file = "gitpython-3.1.57.tar.gz", hash = "sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488"}, ] [package.dependencies] @@ -1378,54 +1491,56 @@ gitdb = ">=4.0.1,<5" [package.extras] doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] +test = ["basedpyright (==1.39.9) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] name = "google-auth" -version = "2.55.2" +version = "2.62.0" description = "Google Authentication Library" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_auth-2.55.2-py3-none-any.whl", hash = "sha256:d715f265f2cafc6a5f1bf0dc19870d20e3119f6f6682785a250bce3d03d38a3b"}, - {file = "google_auth-2.55.2.tar.gz", hash = "sha256:97ae7790ff740f2bc9db60eb864a7804f4ac19f5f02c38b3d942f2fea6e9b9ae"}, + {file = "google_auth-2.62.0-py3-none-any.whl", hash = "sha256:4ff4319aeb4ad128409759d397a9fcafad126d0031d241cc0dd6b9a00b43e3f3"}, + {file = "google_auth-2.62.0.tar.gz", hash = "sha256:0bef0ce54bdf9ce226c5d66e4264413bd918141c31bbe49fb52eac882f513d69"}, ] [package.dependencies] -cryptography = ">=38.0.3" +cryptography = [ + {version = ">=38.0.3", markers = "python_version < \"3.14\""}, + {version = ">=41.0.5", markers = "python_version >= \"3.14\""}, +] pyasn1-modules = ">=0.2.1" -requests = {version = ">=2.20.0,<3.0.0", optional = true, markers = "extra == \"requests\""} +requests = {version = ">=2.30.0,<3.0.0", optional = true, markers = "extra == \"requests\""} [package.extras] -aiohttp = ["aiohttp (>=3.8.0,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] -cryptography = ["cryptography (>=38.0.3)"] -enterprise-cert = ["cryptography (>=38.0.3)"] +aiohttp = ["aiohttp (>=3.8.0,<4.0.0) ; python_version < \"3.14\"", "aiohttp (>=3.9.0,<4.0.0) ; python_version >= \"3.14\"", "requests (>=2.30.0,<3.0.0)"] +cryptography = ["cryptography (>=38.0.3) ; python_version < \"3.14\"", "cryptography (>=41.0.5) ; python_version >= \"3.14\""] +enterprise-cert = ["cryptography (>=38.0.3) ; python_version < \"3.14\"", "cryptography (>=41.0.5) ; python_version >= \"3.14\""] grpc = ["grpcio (>=1.59.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""] pyjwt = ["pyjwt (>=2.0)"] -pyopenssl = ["cryptography (>=38.0.3)"] +pyopenssl = ["cryptography (>=38.0.3) ; python_version < \"3.14\"", "cryptography (>=41.0.5) ; python_version >= \"3.14\""] reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0)"] -rsa = ["rsa (>=3.1.4,<5)"] -testing = ["aiohttp (>=3.8.0,<4.0.0)", "aioresponses", "flask", "freezegun", "grpcio (>=1.59.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "packaging", "pyjwt (>=2.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] -urllib3 = ["packaging", "urllib3"] +requests = ["requests (>=2.30.0,<3.0.0)"] +testing = ["aiohttp (>=3.8.0,<4.0.0) ; python_version < \"3.14\"", "aiohttp (>=3.9.0,<4.0.0) ; python_version >= \"3.14\"", "aioresponses", "flask", "freezegun", "grpcio (>=1.59.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "packaging (>=20.0)", "pyjwt (>=2.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.30.0,<3.0.0)", "responses", "urllib3 (>=1.26.15,<3.0.0)"] +urllib3 = ["packaging (>=20.0)", "urllib3 (>=1.26.15,<3.0.0)"] [[package]] name = "google-genai" -version = "2.12.1" +version = "2.29.0" description = "GenAI Python SDK" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "google_genai-2.12.1-py3-none-any.whl", hash = "sha256:686d5ec39bda345151d3ed1bac3915f01f49138b1ea519af2eb98f11cc55ebc4"}, - {file = "google_genai-2.12.1.tar.gz", hash = "sha256:78c25217885d63dc430ca7c4526853512b164a25a93a8a0d0af5b85971aa1db0"}, + {file = "google_genai-2.29.0-py3-none-any.whl", hash = "sha256:7adda55cb6746fb46d38ad3202c3372840b96eede964b5fb3df1469cd4998570"}, + {file = "google_genai-2.29.0.tar.gz", hash = "sha256:124d3eef9ff6b9e4ea548f95ff5b7721683e7990c0a936b006d9e2ab61ea897d"}, ] [package.dependencies] anyio = ">=4.8.0,<5.0.0" distro = ">=1.7.0,<2" -google-auth = {version = ">=2.48.1,<2.56.0", extras = ["requests"]} +google-auth = {version = ">=2.56.0,<3.0.0", extras = ["requests"]} httpx = ">=0.28.1,<1.0.0" pydantic = ">=2.12.5,<3.0.0" requests = ">=2.28.1,<3.0.0" @@ -1564,7 +1679,6 @@ files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] -markers = {main = "extra == \"docs\""} [package.dependencies] MarkupSafe = ">=2.0" @@ -1684,6 +1798,19 @@ files = [ {file = "jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4"}, ] +[[package]] +name = "legacy-cgi" +version = "2.6.4" +description = "Fork of the standard library cgi and cgitb modules removed in Python 3.13" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd"}, + {file = "legacy_cgi-2.6.4.tar.gz", hash = "sha256:abb9dfc7835772f7c9317977c63253fd22a7484b5c9bbcdca60a29dcce97c577"}, +] + [[package]] name = "librt" version = "0.7.8" @@ -1788,6 +1915,30 @@ markers = {main = "extra == \"docs\""} docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1886,7 +2037,18 @@ files = [ {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] -markers = {main = "extra == \"docs\""} + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] [[package]] name = "mergedeep" @@ -1903,14 +2065,14 @@ markers = {main = "extra == \"docs\""} [[package]] name = "mistune" -version = "3.3.3" +version = "3.3.4" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "mistune-3.3.3-py3-none-any.whl", hash = "sha256:99de1585e42dcbd826faa9e11a202727a5e202e4e4722a4c69ac1ff615793dd7"}, - {file = "mistune-3.3.3.tar.gz", hash = "sha256:c4c6c0c840b8637a2e9b8b6d607eb7c8f00888bf14c754409bcd339e848c2477"}, + {file = "mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a"}, + {file = "mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe"}, ] [package.dependencies] @@ -2056,14 +2218,14 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "2.0.5" +version = "2.0.9" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.10" groups = ["main", "docs"] files = [ - {file = "mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d"}, - {file = "mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594"}, + {file = "mkdocstrings_python-2.0.9-py3-none-any.whl", hash = "sha256:c0233eff3f84d78110df50541918e7ec2bcff8e1614faddea34290a90321a416"}, + {file = "mkdocstrings_python-2.0.9.tar.gz", hash = "sha256:ae945637dc0618c6beedbee169f10c0234b77f322c53db3fc9fb4c29b3013038"}, ] markers = {main = "extra == \"docs\""} @@ -2676,7 +2838,7 @@ description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" groups = ["main"] -markers = "sys_platform == \"linux\"" +markers = "sys_platform != \"win32\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -2984,14 +3146,14 @@ typing-extensions = ">=4.14.1" [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs"] files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [package.extras] @@ -3117,7 +3279,6 @@ files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] -markers = {main = "extra == \"docs\""} [package.dependencies] six = ">=1.5" @@ -3137,6 +3298,25 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-slugify" +version = "9.1.3" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "python_slugify-9.1.3-py3-none-any.whl", hash = "sha256:9aced0670e54c5603e2335c0fa9a4011ad4bc41ecb3c46e8097294c46fd60ed4"}, + {file = "python_slugify-9.1.3.tar.gz", hash = "sha256:90e997f2e0987239ce95e12f700086eb18e1d1d3ee22624fbbdbd095afca42b6"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +anyascii = ["anyascii (>=0.3.2)"] +unidecode = ["Unidecode (>=1.1.1)"] + [[package]] name = "python-tkdnd" version = "0.2.1" @@ -3311,7 +3491,6 @@ files = [ {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] -markers = {main = "extra == \"docs\""} [[package]] name = "pyyaml-env-tag" @@ -3351,6 +3530,25 @@ urllib3 = ">=1.26,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "scikit-build" version = "0.18.1" @@ -3422,7 +3620,6 @@ files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -markers = {main = "extra == \"docs\""} [[package]] name = "smmap" @@ -3505,6 +3702,18 @@ files = [ doc = ["reno", "sphinx"] test = ["pytest", "tornado (>=4.5)", "typeguard"] +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + [[package]] name = "tinycss2" version = "1.5.1" @@ -3793,6 +4002,18 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "tzdata" +version = "2026.5" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2026.5-py2.py3-none-any.whl", hash = "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac"}, + {file = "tzdata-2026.5.tar.gz", hash = "sha256:8cc73c0a0bfca7dbfa59235d60b2eff82231dee33f53d206db1acd9173cfc0a7"}, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -4115,4 +4336,4 @@ docs = ["mkdocs", "mkdocs-material", "mkdocstrings-python"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "0c0795bc3de09cfe7840c6d3966e0ed80fe1e35eb9737cc7b2e0e18bc9e2707f" +content-hash = "64d0a3311ffa32fb6c8e8cc55e9ce4bd9290269ebb034a77b64329ee58e7e89d" diff --git a/pyproject.toml b/pyproject.toml index 77e0a7ea..35476448 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,10 +29,15 @@ dependencies = [ "tarts>=0.20.1,<0.21.0", "editorconfig>=0.17.1,<0.18.0", "tkextrafont>=0.6.3,<0.7.0; sys_platform == 'win32'", - "ptyprocess>=0.7.0,<0.8.0; sys_platform == 'linux'", + "ptyprocess>=0.7.0,<0.8.0; sys_platform != 'win32'", "anthropic>=0.117.0,<0.118.0", "tree-sitter>=0.26.0,<0.27.0", "tree-sitter-language-pack>=1.12.5,<2.0.0", + "legacy-cgi>=2.6.0; python_version >= '3.13'", + "cookiecutter>=2.6.0", + "dap-python>=1.0.1,<2.0.0; python_version >= '3.12'", + "pydantic>=2.10.0,<3.0.0", + "debugpy>=1.8.0,<2.0.0", ] [project.optional-dependencies] @@ -58,29 +63,33 @@ python = "^3.10" hintedtext = "^0.6.0" watchdog = "^6.0.0" filetype = "^1.2.0" -gitpython = "^3.1.52" -pygments = "^2.20.0" +gitpython = "^3.1.57" +pygments = "^2.21.0" toml = "^0.10.2" sv-ttk = "^2.6.1" pillow = "^12.3.0" requests = "^2.34.2" pyperclip = "^1.11.0" tkinterweb = {version = "^4.25.3", extras = ["full"]} -mistune = "^3.3.3" +mistune = "^3.3.4" chardet = "^7.4.3" pywinpty = { version = "^3.0.5", platform = "win32" } python-tkdnd = "^0.2.1" psutil = "^7.2.2" -google-genai = "^2.12.1" +google-genai = "^2.29.0" sortedcontainers = "^2.4.0" -click = "^8.4.2" +click = "^8.5.0" tarts = "^0.20.1" editorconfig = "^0.17.1" tkextrafont = { version = "^0.6.3", platform = "win32" } -ptyprocess = { version = "^0.7.0", platform = "linux" } -anthropic = "^0.117.0" +ptyprocess = { version = "^0.7.0", markers = "sys_platform != 'win32'" } +anthropic = "^0.117.1" tree-sitter = "^0.26.0" -tree-sitter-language-pack = "^1.12.5" +tree-sitter-language-pack = "^1.22.1" +legacy-cgi = { version = ">=2.6.0", python = ">=3.13" } +dap-python = { version = "^1.0.1", python = ">=3.12" } +pydantic = "^2.10.0" +debugpy = "^1.8.0" [tool.poetry.group.dev.dependencies] pytest = "^9.1.1" @@ -93,7 +102,7 @@ optional = true [tool.poetry.group.docs.dependencies] mkdocs = "^1.6.1" -mkdocstrings-python = "^2.0.5" +mkdocstrings-python = "^2.0.9" mkdocs-material = "^9.7.7" [dependency-groups] @@ -106,6 +115,9 @@ dev = [ [tool.pytest.ini_options] asyncio_mode = "auto" +markers = [ + "integration: marks integration tests", +] [build-system] requires = ["poetry-core"] diff --git a/requirements.txt b/requirements.txt index 7b7fa60a..1e1c6ba9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ gitpython filetype -tkextrafont +tkextrafont; sys_platform == "win32" watchdog pygments Pillow diff --git a/scripts/linux.spec b/scripts/linux.spec index 71e55b4d..26be2a1e 100644 --- a/scripts/linux.spec +++ b/scripts/linux.spec @@ -51,5 +51,5 @@ exe = EXE( target_arch=None, codesign_identity=None, entitlements_file=None, - icon=['../resources/windows/logo.png'], + icon=['../resources/linux/logo.png'], ) diff --git a/src/biscuit/api/__init__.py b/src/biscuit/api/__init__.py index 9d21fdf8..f640de75 100644 --- a/src/biscuit/api/__init__.py +++ b/src/biscuit/api/__init__.py @@ -1 +1,2 @@ from .base import ExtensionsAPI +from .settings import Settings diff --git a/src/biscuit/api/base.py b/src/biscuit/api/base.py index 2cd0a01a..43516121 100644 --- a/src/biscuit/api/base.py +++ b/src/biscuit/api/base.py @@ -8,6 +8,8 @@ from .logger import Logger from .notifications import Notifications from .releases import Releases +from .settings import Settings +from .theme import Theme from .views import Views if typing.TYPE_CHECKING: @@ -57,11 +59,15 @@ def __init__(self, base: App) -> None: self.views = Views(self.base) self.releases = Releases(self.base) self.assistant = Assistant(self.base) + self.settings = Settings(self.base) + self.theme = Theme(self.base) self.register_comment_prefix = self.base.register_comment_prefix self.register_game = self.base.register_game self.register_langserver = self.base.register_langserver self.register_run_command = self.base.register_run_command + self.register_debugger = self.base.register_debugger + self.register_dap_adapter = self.base.register_dap_adapter def register(self, name: str, extension: object) -> None: """Register an extension""" diff --git a/src/biscuit/api/settings.py b/src/biscuit/api/settings.py new file mode 100644 index 00000000..5c8e44a3 --- /dev/null +++ b/src/biscuit/api/settings.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import typing +from typing import Any + +from .endpoint import Endpoint + +if typing.TYPE_CHECKING: + from biscuit import App + + +class Settings(Endpoint): + """Settings endpoint for Extension API. + + Allows extensions to: + - register new custom settings + - retrieve configuration values + - set configuration values + - query registered settings + """ + + def __init__(self, base: App) -> None: + super().__init__(base) + self._custom_settings: dict[str, dict[str, Any]] = {} + + def get(self, key: str, default: Any = None) -> Any: + """Retrieve a setting value from application config.""" + return self.base.settings.config.get_value(key, default) + + def set(self, key: str, value: Any) -> None: + """Set a configuration value.""" + self.base.settings.config.data[key] = value + + def register_setting( + self, + name: str, + type: str = "string", + default: Any = None, + description: str = "", + ) -> None: + """Register a new setting option for extensions. + + Args: + name (str): The configuration key name. + type (str): Type of setting ("string", "boolean", "integer", "number", "list"). + default (Any): Default value if not set. + description (str): Human-readable explanation. + """ + self._custom_settings[name] = { + "name": name, + "type": type, + "default": default, + "description": description, + } + if name not in self.base.settings.config.data and default is not None: + self.base.settings.config.data[name] = default + + def get_registered_settings(self) -> dict[str, dict[str, Any]]: + """Return all custom settings registered by extensions.""" + return self._custom_settings.copy() + + def get_all(self) -> dict[str, Any]: + """Return all current config key-values.""" + return self.base.settings.config.data.copy() diff --git a/src/biscuit/api/theme.py b/src/biscuit/api/theme.py index 94a2d2db..8829bf4d 100644 --- a/src/biscuit/api/theme.py +++ b/src/biscuit/api/theme.py @@ -5,7 +5,11 @@ class Theme(Endpoint): def __init__(self, *a) -> None: super().__init__(*a) - # TODO themes registration, api - def register_theme(self, theme): ... + def register_theme(self, name: str, theme) -> None: + """Register a new theme class or instance.""" + self.base.settings.config.register_theme(name, theme) + + def set_theme(self, theme: str) -> None: + """Set the active theme at runtime.""" + self.base.settings.config.set_theme(theme) - def set_theme(self, theme): ... diff --git a/src/biscuit/cli/cli.py b/src/biscuit/cli/cli.py index 6bfaff04..44e3b3d7 100644 --- a/src/biscuit/cli/cli.py +++ b/src/biscuit/cli/cli.py @@ -19,10 +19,11 @@ def main(self, *args, **kwargs): return super(BiscuitCLI, self).main(*args, **kwargs) except Exception: path = self.path + if not path: + raise - if path: - path = Path(self.path).resolve() - path.mkdir(exist_ok=True) + path = Path(self.path).resolve() + path.mkdir(exist_ok=True) appdir = Path(__file__).resolve().parent app = get_app_instance(appdir, open_path=str(path)) @@ -57,9 +58,10 @@ def cli(path=None, dev=False): Run `biscuit ` to open a folder/file (`biscuit .` for this directory) """ - - click.echo(f"Biscuit v{__version__} {'(dev) 🚧' if dev else '🚀'}") - + try: + click.echo(f"Biscuit v{__version__} {'(dev) 🚧' if dev else '🚀'}") + except UnicodeEncodeError: + click.echo(f"Biscuit v{__version__} {'(dev)' if dev else ''}") @cli.result_callback() def process_commands(processors, path=None, dev=False): diff --git a/src/biscuit/cli/extensions.py b/src/biscuit/cli/extensions.py index 60511304..37ded1d9 100644 --- a/src/biscuit/cli/extensions.py +++ b/src/biscuit/cli/extensions.py @@ -3,6 +3,7 @@ import importlib.util as _importlib_util import re import sys +import time import typing from pathlib import Path @@ -53,8 +54,13 @@ def process_extension_commands(ctx, processors): app.withdraw() print("\nLoading extensions...") - while not app.extensions_manager.extensions_loaded: - ... + start_time = time.time() + while not app.extensions_manager.extensions_loaded and time.time() - start_time < 5: + try: + app.update() + except Exception: + pass + time.sleep(0.05) if isinstance(processors, list): for processor in processors: @@ -88,7 +94,8 @@ def f(app: App, user=user) -> None: for i, data in enumerate( app.extensions_manager.list_extensions_by_user(user) ): - click.echo(f"[{i}] {data[0]}: " + data[1][-1]) + desc = data[1].get("description", "") if isinstance(data[1], dict) else data[1][-1] + click.echo(f"[{i}] {data[0]}: " + desc) return f @@ -99,9 +106,17 @@ def f(app: App) -> None: for i, data in enumerate( app.extensions_manager.list_installed_extensions() ): - click.echo( - f"[{i}] {data[0]}: " + ", ".join(data[1]) if data[1] else " ... " - ) + name = data[0] + details = data[1] + if isinstance(details, dict): + version_str = f" [v{details['version']}]" if "version" in details and details["version"] else "" + desc = details.get("description", " ... ") + click.echo(f"[{i}] {name}{version_str}: {desc}") + else: + version_str = f" [v{details[3]}]" if details and len(details) > 3 and details[3] else "" + click.echo( + f"[{i}] {name}{version_str}: " + (", ".join(details) if details else " ... ") + ) return f else: @@ -109,7 +124,14 @@ def f(app: App) -> None: def f(app: App) -> None: for i, data in enumerate(app.extensions_manager.list_all_extensions()): - click.echo(f"[{i}] {data[0]}: " + ", ".join(data[1][1:])) + name = data[0] + details = data[1] + if isinstance(details, dict): + version_str = f" [v{details['version']}]" if "version" in details and details["version"] else "" + desc = details.get("description", "") + click.echo(f"[{i}] {name}{version_str}: {desc}") + else: + click.echo(f"[{i}] {data[0]}: " + ", ".join(data[1][1:])) return f @@ -130,10 +152,18 @@ def info(name: str | None) -> typing.Callable[[App], None]: def f(app: App, name=name) -> None: data = app.extensions_manager.find_extension_by_name(name) if data: - click.echo(f"Name: {name}") - click.echo(f"Author: {data[1]}") - click.echo(f"Description: {data[2]}") - # TODO: click.echo(f"Version: {data[3]}") + if isinstance(data, dict): + click.echo(f"Name: {data.get('name', name)}") + click.echo(f"Author: {data.get('author', '')}") + click.echo(f"Description: {data.get('description', '')}") + if "version" in data and data["version"]: + click.echo(f"Version: {data['version']}") + else: + click.echo(f"Name: {name}") + click.echo(f"Author: {data[1]}") + click.echo(f"Description: {data[2]}") + if len(data) > 3 and data[3]: + click.echo(f"Version: {data[3]}") else: click.echo(f"Could not find extension {name}") @@ -186,6 +216,137 @@ def f(app: App, name=name) -> None: return f +@ext.command() +@click.argument("name", required=False) +def update(name: str | None) -> typing.Callable[[App], None]: + """Update an installed extension or all outdated extensions. + + Example:: + + biscuit ext update + biscuit ext update extension_name + + Args:: + + name (str, optional): The name of the extension to update + """ + + def f(app: App, name=name) -> None: + if name: + if app.extensions_manager.update_extension_from_name(name): + click.echo(f"Updated extension {name} successfully") + else: + click.echo(f"Could not find or update extension {name}") + else: + outdated = app.extensions_manager.check_outdated_extensions() + if not outdated: + click.echo("All installed extensions are up to date.") + return + click.echo(f"Found {len(outdated)} outdated extension(s). Updating...") + for ext_id, info in outdated.items(): + if app.extensions_manager.update_extension_from_name(ext_id): + click.echo(f"Updated {ext_id} to v{info['available_version']}") + else: + click.echo(f"Failed to update {ext_id}") + + return f + + +@ext.command("deps") +@click.argument("name", required=False) +@click.option("--install", is_flag=True, help="Install dependencies into isolated .deps") +@click.option("--force", is_flag=True, help="Force reinstallation even if already present") +def deps(name: str | None, install: bool, force: bool) -> typing.Callable[[App], None] | None: + """Inspect and manage extension dependencies. + + Example:: + + biscuit ext deps + biscuit ext deps python + biscuit ext deps python --install + biscuit ext deps --install + """ + from biscuit.extensions.dependencies import DependencyManager + dep_mgr = DependencyManager() + + cwd = Path.cwd() + # Check if run directly in an extension development directory + if not name and ((cwd / "extension.toml").exists() or (cwd / "src").is_dir()): + python_pkgs, tools = dep_mgr.get_declared_dependencies(cwd) + ext_name = cwd.name + click.echo(f"Extension '{ext_name}' (local dev):") + if python_pkgs: + click.echo(f" Python packages: {', '.join(python_pkgs)}") + deps_dir = cwd / ".deps" + installed = (deps_dir / ".installed").exists() + click.echo(f" Isolated .deps status: {'Installed' if installed else 'Not installed'}") + else: + click.echo(" Python packages: (none declared)") + + if tools: + missing = dep_mgr.check_tools(ext_name, tools) + tool_status = [f"{t} ({'found' if t not in missing else 'MISSING'})" for t in tools] + click.echo(f" System tools: {', '.join(tool_status)}") + else: + click.echo(" System tools: (none declared)") + + if install: + click.echo(f"Installing dependencies for '{ext_name}'...") + ok = dep_mgr.ensure_dependencies(cwd, force=force) + if ok: + click.echo("Dependencies successfully resolved.") + else: + click.echo("Failed to resolve dependencies.") + return None + + def f(app: App, name=name, install=install, force=force) -> None: + mgr = app.extensions_manager + targets = [] + if name: + target_dir = mgr.extension_dir / "extensions" / name + if not target_dir.exists(): + click.echo(f"Extension '{name}' is not installed locally.") + return + targets.append((name, target_dir)) + else: + for ext_id in mgr.installed: + target_dir = mgr.extension_dir / "extensions" / ext_id + if target_dir.exists(): + targets.append((ext_id, target_dir)) + + if not targets: + click.echo("No installed extensions found.") + return + + for ext_id, ext_dir in targets: + python_pkgs, tools = mgr.dependency_manager.get_declared_dependencies(ext_dir) + click.echo(f"Extension '{ext_id}':") + if python_pkgs: + click.echo(f" Python packages: {', '.join(python_pkgs)}") + deps_dir = ext_dir / ".deps" + installed = (deps_dir / ".installed").exists() + click.echo(f" Isolated .deps status: {'Installed' if installed else 'Not installed'}") + else: + click.echo(" Python packages: (none declared)") + + if tools: + missing = mgr.dependency_manager.check_tools(ext_id, tools) + tool_status = [f"{t} ({'found' if t not in missing else 'MISSING'})" for t in tools] + click.echo(f" System tools: {', '.join(tool_status)}") + else: + click.echo(" System tools: (none declared)") + + if install: + click.echo(f"Installing dependencies for '{ext_id}'...") + ok = mgr.ensure_dependencies(ext_dir, force=force) + if ok: + click.echo(f"Dependencies for '{ext_id}' successfully installed.") + else: + click.echo(f"Failed to install dependencies for '{ext_id}'.") + + return f + + @ext.command() @click.argument("name", required=False) @click.option( @@ -309,6 +470,11 @@ def f(app: App) -> None: ) return + from biscuit.extensions.dependencies import DependencyManager + dep_mgr = DependencyManager() + dep_mgr.ensure_dependencies(cwd) + dep_mgr.add_to_sys_path(cwd) + for p in (str(cwd), str(src_dir)): if p not in sys.path: sys.path.insert(0, p) @@ -370,7 +536,7 @@ def test(keyword: str | None, verbose: bool): click.echo("Running pytest…") - args: list[str] = [] + args: list[str] = ["--rootdir", str(cwd), str(cwd / "tests")] if verbose: args.append("-v") if keyword: @@ -379,7 +545,10 @@ def test(keyword: str | None, verbose: bool): exit_code = pytest.main(args, plugins=None) if exit_code == 0: - click.echo("All tests passed! 🎉") + try: + click.echo("All tests passed! 🎉") + except UnicodeEncodeError: + click.echo("All tests passed!") else: click.echo(f"Tests failed with exit code {exit_code}.") sys.exit(exit_code) @@ -492,9 +661,9 @@ def publish(skip_tests: bool): return None -@ext.command() +@ext.command("publish-update") @click.option("--skip-tests", is_flag=True, help="Skip running tests before preparing update instructions.") -def update(skip_tests: bool): +def publish_update(skip_tests: bool): """Guide for updating an *already published* extension. Produces a checklist for bumping your extension to a new version in the diff --git a/src/biscuit/commands.py b/src/biscuit/commands.py index 3f0754cf..3b81b68b 100644 --- a/src/biscuit/commands.py +++ b/src/biscuit/commands.py @@ -42,15 +42,28 @@ def new_window(self, *_) -> None: def open_empty_editor(self, *_) -> None: self.new_file() + def _safe_dialog_options(self, **kwargs) -> dict: + opts = kwargs.copy() + if "parent" not in opts: + opts["parent"] = self.base + try: + self.base.option_add("*Dialog*background", self.base.theme.secondary_background) + self.base.option_add("*Dialog*foreground", self.base.theme.primary_foreground) + self.base.option_add("*FileDialog*background", self.base.theme.secondary_background) + self.base.option_add("*FileDialog*foreground", self.base.theme.primary_foreground) + except Exception: + pass + return opts + def open_file(self, *_) -> None: - path = filedialog.askopenfilename() + path = filedialog.askopenfilename(**self._safe_dialog_options()) if not path or not os.path.isfile(path): return self.base.open_editor(path) self.base.history.register_file_history(path) def open_directory(self, *_) -> None: - path = filedialog.askdirectory() + path = filedialog.askdirectory(**self._safe_dialog_options()) if not path or not os.path.isdir(path): return self.base.open_directory(path) @@ -81,8 +94,10 @@ def save_file_as(self, *_) -> None: if editor := self.base.editorsmanager.active_editor: if editor.content and editor.content.editable: if path := asksaveasfilename( - title="Save As...", - initialfile=(editor.filename or "Untitled.txt"), + **self._safe_dialog_options( + title="Save As...", + initialfile=(editor.filename or "Untitled.txt"), + ) ): editor.save(path) @@ -91,8 +106,10 @@ def save_all(self, *_) -> None: if editor.content: if not editor.content.exists: if path := asksaveasfilename( - title="Save As...", - initialfile=(editor.filename or "Untitled.txt"), + **self._safe_dialog_options( + title="Save As...", + initialfile=(editor.filename or "Untitled.txt"), + ) ): return editor.save(path) if editor.content.editable: @@ -100,20 +117,22 @@ def save_all(self, *_) -> None: def save_workspace_as(self, *_) -> None: if path := asksaveasfilename( - title="Save Workspace As...", - initialfile="workspace.toml", - filetypes=[(".toml")], + **self._safe_dialog_options( + title="Save Workspace As...", + initialfile="workspace.toml", + filetypes=[(".toml")], + ) ): self.base.workspaces.save(path) def add_folder_to_workspace(self, *_) -> None: - path = filedialog.askdirectory() + path = filedialog.askdirectory(**self._safe_dialog_options()) if not path or not os.path.isdir(path): return self.base.workspaces.add_dir(path) def open_workspace(self, *_) -> None: - path = filedialog.askopenfilename() + path = filedialog.askopenfilename(**self._safe_dialog_options()) if not path or not os.path.isfile(path): return self.base.workspaces.load(path) @@ -469,6 +488,9 @@ def change_encoding(self, *_) -> None: def change_indentation_level(self, *_) -> None: self.base.palette.show("indent:") + def change_font_size(self, *_) -> None: + self.base.palette.show("zoom:") + def clone_git_repository(self, *_) -> None: self.base.palette.show("clone:") @@ -478,6 +500,18 @@ def goto_line_column(self, *_) -> None: def change_git_branch(self, *_) -> None: self.base.palette.show("branch:") + def git_stash(self, *_) -> None: + if self.base.git and self.base.git.repo: + self.base.git.repo.stash() + if self.base.notifications: + self.base.notifications.info("Git: Stashed changes") + + def git_stash_pop(self, *_) -> None: + if self.base.git and self.base.git.repo: + self.base.git.repo.stash_pop() + if self.base.notifications: + self.base.notifications.info("Git: Applied and dropped latest stash") + @command_palette_ignore def show_run_config_palette(self, command) -> None: self.base.palette.show("runconf:", command) diff --git a/src/biscuit/common/ai/agent.py b/src/biscuit/common/ai/agent.py index a9886fd3..e26e6c6c 100644 --- a/src/biscuit/common/ai/agent.py +++ b/src/biscuit/common/ai/agent.py @@ -78,6 +78,7 @@ def __init__(self, base: "App", api_key: str, model_name: str): self.is_running = False self.current_task: Optional[AgentTask] = None self.chat_history: List[Dict[str, str]] = [] + self._native_history: List[Dict[str, Any]] = [] self.attached_files: List[str] = [] self.max_steps = 20 @@ -154,6 +155,23 @@ def _prepare_context(self, user_input: str) -> str: parts.append("\n".join(ctx_parts)) return "\n".join(parts) + def _get_project_instructions(self) -> str: + instructions = [] + if hasattr(self, "base") and hasattr(self.base, "config"): + custom = self.base.config.get_nested("ai.system_prompt", "") + if custom: + instructions.append(f"## Custom User Instructions\n{custom}") + if hasattr(self, "base") and hasattr(self.base, "active_directory") and self.base.active_directory: + for candidate in [".biscuit/agent.md", ".biscuit/instructions.md"]: + p = os.path.join(self.base.active_directory, candidate) + if os.path.exists(p): + try: + with open(p, "r", encoding="utf-8") as f: + instructions.append(f"## Project Instructions ({candidate})\n{f.read()}") + except Exception: + pass + return "\n\n".join(instructions) + def _get_system_instruction(self) -> str: active_editor = getattr(self.base.editorsmanager.active_editor, "path", "None") workspace_path = getattr(self.base, "active_directory", os.getcwd()) @@ -166,7 +184,9 @@ def _get_system_instruction(self) -> str: f"Attached Files: {', '.join(os.path.basename(f) for f in self.attached_files)}" ) context = " | ".join(context_parts) - return f"{self.SYSTEM_PROMPT}\n\n## Current Context\n{context}" + base_sys = f"{self.SYSTEM_PROMPT}\n\n## Current Context\n{context}" + proj = self._get_project_instructions() + return f"{base_sys}\n\n{proj}" if proj else base_sys async def execute_task_with_streaming(self, description: str, **kwargs) -> AgentTask: if self.is_running: @@ -203,15 +223,25 @@ async def execute_task_with_streaming(self, description: str, **kwargs) -> Agent return task + def _get_message_history(self) -> List[Dict[str, Any]]: + """Copy native history, falling back to the existing text-only history.""" + if self._native_history: + return list(self._native_history) + return [ + { + "role": "user" if msg["role"] == "User" else "assistant", + "content": msg["content"], + } + for msg in self.chat_history[-10:] + ] + async def _run_chat_session(self, task: AgentTask, inputs: str): """Execute task with provider-native tool calling and real-time streaming.""" tools_map = self._tools_map() system_instruction = self._get_system_instruction() tool_descs = self._all_tool_descs() - messages: List[Dict[str, Any]] = [] - for msg in self.chat_history[-10:]: - messages.append({"role": "user" if msg["role"] == "User" else "assistant", "content": msg["content"]}) + messages = self._get_message_history() messages.append({"role": "user", "content": inputs}) for i in range(self.max_steps): @@ -241,10 +271,19 @@ async def _run_chat_session(self, task: AgentTask, inputs: str): thought_duration = time.time() - thought_start self._stream_content(f"[END_THOUGHT] {thought_duration:.1f}") - assistant_msg: Dict[str, Any] = {"role": "assistant", "content": response.text or ""} + assistant_msg: Dict[str, Any] = { + "role": "assistant", + "content": ( + response.assistant_content + if response.assistant_content is not None + else response.text or "" + ), + } if not response.tool_calls: messages.append(assistant_msg) + if response.assistant_content is not None: + self._native_history = messages self.chat_history.append({"role": "User", "content": inputs}) self.chat_history.append({"role": "AI", "content": response.text or ""}) return @@ -294,3 +333,6 @@ def process_message_sync(self, message: str) -> str: def stop_execution(self): self.is_running = False + self.is_aborted = True + self._stream_content('\n\n*[Execution stopped by user]*') + diff --git a/src/biscuit/common/ai/diff_review.py b/src/biscuit/common/ai/diff_review.py new file mode 100644 index 00000000..5124e4db --- /dev/null +++ b/src/biscuit/common/ai/diff_review.py @@ -0,0 +1,35 @@ +from __future__ import annotations +import difflib +from dataclasses import dataclass +from typing import Dict, List, Optional + +@dataclass +class PendingEdit: + path: str + original_content: str + proposed_content: str + + def diff(self) -> str: + orig_lines = self.original_content.splitlines(keepends=True) + prop_lines = self.proposed_content.splitlines(keepends=True) + return "".join(difflib.unified_diff( + orig_lines, prop_lines, fromfile=f"a/{self.path}", tofile=f"b/{self.path}" + )) + +class DiffReviewManager: + def __init__(self): + self.pending_edits: Dict[str, PendingEdit] = {} + + def stage_edit(self, path: str, original_content: str, proposed_content: str) -> PendingEdit: + edit = PendingEdit(path, original_content, proposed_content) + self.pending_edits[path] = edit + return edit + + def accept_edit(self, path: str) -> Optional[PendingEdit]: + return self.pending_edits.pop(path, None) + + def reject_edit(self, path: str) -> Optional[PendingEdit]: + return self.pending_edits.pop(path, None) + + def list_pending(self) -> List[PendingEdit]: + return list(self.pending_edits.values()) diff --git a/src/biscuit/common/ai/inline_completion.py b/src/biscuit/common/ai/inline_completion.py new file mode 100644 index 00000000..0b69a11e --- /dev/null +++ b/src/biscuit/common/ai/inline_completion.py @@ -0,0 +1,27 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Optional + +@dataclass +class InlineCompletionState: + prefix: str + suffix: str + suggestion: Optional[str] = None + is_active: bool = False + + def trigger(self, prefix: str, suffix: str, suggestion: str): + self.prefix = prefix + self.suffix = suffix + self.suggestion = suggestion + self.is_active = True + + def accept(self) -> Optional[str]: + if not self.is_active: + return None + res = self.suggestion + self.dismiss() + return res + + def dismiss(self): + self.suggestion = None + self.is_active = False diff --git a/src/biscuit/common/ai/providers/__init__.py b/src/biscuit/common/ai/providers/__init__.py index 8ea1c0c1..fe711a72 100644 --- a/src/biscuit/common/ai/providers/__init__.py +++ b/src/biscuit/common/ai/providers/__init__.py @@ -19,6 +19,8 @@ def register(provider_class: Type[AIProvider]) -> None: _providers[provider_class.name] = provider_class def get_provider_for_model(model_id: str) -> Optional[Type[AIProvider]]: + if not _providers: + _auto_register() for cls in _providers.values(): if cls.supports_model(model_id): return cls @@ -36,10 +38,13 @@ def resolve(model_id: str, api_key: str) -> AIProvider: ) return cls(api_key, model_id) -def list_models() -> Dict[str, str]: +def list_models(selected_model: Optional[str] = None) -> Dict[str, str]: models = {} for cls in _providers.values(): models.update(cls.get_models()) + if selected_model and selected_model not in models.values(): + # Retain an explicit saved/custom selection without advertising it globally. + models[selected_model] = selected_model return models def list_provider_names() -> List[str]: @@ -59,7 +64,11 @@ def _auto_register() -> None: from .anthropic import AnthropicProvider from .groq import GroqProvider from .minimax import MiniMaxProvider + from .ollama import OllamaProvider + from .openai_compatible import OpenAICompatibleProvider register(GeminiProvider) register(AnthropicProvider) register(GroqProvider) register(MiniMaxProvider) + register(OllamaProvider) + register(OpenAICompatibleProvider) diff --git a/src/biscuit/common/ai/providers/base.py b/src/biscuit/common/ai/providers/base.py index da795659..7c2003d0 100644 --- a/src/biscuit/common/ai/providers/base.py +++ b/src/biscuit/common/ai/providers/base.py @@ -19,6 +19,8 @@ class ProviderResponse: tool_calls: Optional[List[ToolCallData]] = None input_tokens: int = 0 output_tokens: int = 0 + # Optional native blocks for providers that require complete tool history. + assistant_content: Optional[List[Dict[str, Any]]] = None @dataclass diff --git a/src/biscuit/common/ai/providers/minimax.py b/src/biscuit/common/ai/providers/minimax.py index 74df1233..0b66c8d3 100644 --- a/src/biscuit/common/ai/providers/minimax.py +++ b/src/biscuit/common/ai/providers/minimax.py @@ -14,11 +14,16 @@ class MiniMaxProvider(AIProvider): name = "minimax" _models: ClassVar[Dict[str, str]] = { - "MiniMax M1": "minimax-m1", - "MiniMax Text-01": "minimax-text-01", + "MiniMax M3.1 Flash": "MiniMax-M3.1-Flash", + "MiniMax M3": "MiniMax-M3", } - def __init__(self, api_key: str, model: str): + @classmethod + def supports_model(cls, model_id: str) -> bool: + """Route explicit MiniMax IDs without advertising legacy/custom models.""" + return model_id.lower().startswith("minimax-") + + def __init__(self, api_key: str, model: str = "MiniMax-M3.1-Flash"): super().__init__(api_key, model) self._client: Optional[anthropic.AsyncAnthropic] = None @@ -67,10 +72,17 @@ def _build_messages(self, messages: List[Dict]) -> List[Dict]: continue if role == "assistant" and tool_calls: - blocks = [] - if content: + # Preserve native blocks, including thinking signatures. + blocks = list(content) if isinstance(content, list) else [] + if content and isinstance(content, str): blocks.append({"type": "text", "text": content}) for tc in tool_calls: + if any( + block.get("type") == "tool_use" + and block.get("id") == tc.get("id") + for block in blocks + ): + continue blocks.append({ "type": "tool_use", "id": tc.get("id", f"toolu_{i}"), @@ -102,46 +114,14 @@ async def generate( system=system_instruction, tools=anthrow_tools or anthropic.NOT_GIVEN, messages=anthrow_messages, - temperature=0, + # Omit thinking/effort: Flash defaults to adaptive/max; + # M3 defaults to thinking off on this API. ) as stream: - current_text = "" async for event in stream: if event.type == "text" and stream_callback: stream_callback(event.text) - current_text += event.text - - final_message = await stream.get_final_message() - - input_tokens = 0 - output_tokens = 0 - if final_message.usage: - input_tokens = final_message.usage.input_tokens or 0 - output_tokens = final_message.usage.output_tokens or 0 - - tool_calls = [ - c for c in final_message.content if c.type == "tool_use" - ] - - if tool_calls: - return ProviderResponse( - text=current_text, - tool_calls=[ - ToolCallData( - name=tc.name, - args=dict(tc.input), - id=tc.id, - ) - for tc in tool_calls - ], - input_tokens=input_tokens, - output_tokens=output_tokens, - ) - - return ProviderResponse( - text=current_text, - input_tokens=input_tokens, - output_tokens=output_tokens, - ) + + return self._parse_response(await stream.get_final_message()) except Exception as e: err_msg = str(e).lower() @@ -151,15 +131,40 @@ async def generate( raise RuntimeError("Invalid MiniMax API key") from e raise RuntimeError(f"MiniMax API error: {e}") from e + @staticmethod + def _parse_response(message: anthropic.types.Message) -> ProviderResponse: + """Expose text and tools while retaining every native block for replay.""" + tool_calls = [ + ToolCallData(name=block.name, args=dict(block.input), id=block.id) + for block in message.content if block.type == "tool_use" + ] + input_tokens = 0 + output_tokens = 0 + if message.usage: + input_tokens = message.usage.input_tokens or 0 + output_tokens = message.usage.output_tokens or 0 + return ProviderResponse( + text="".join( + block.text for block in message.content if block.type == "text" + ), + assistant_content=[ + block.model_dump(exclude_none=True) for block in message.content + ], + tool_calls=tool_calls or None, + input_tokens=input_tokens, + output_tokens=output_tokens, + ) + def process_message(self, message: str) -> str: - import anthropic as _anthropic - client = _anthropic.Anthropic( + with anthropic.Anthropic( api_key=self.api_key, base_url=MINIMAX_ANTHROPIC_BASE_URL, + ) as client: + response = client.messages.create( + model=self.model, + max_tokens=8000, + messages=[{"role": "user", "content": message}], + ) + return "".join( + block.text for block in response.content if block.type == "text" ) - response = client.messages.create( - model=self.model, - max_tokens=1000, - messages=[{"role": "user", "content": message}], - ) - return response.content[0].text if response.content else "" \ No newline at end of file diff --git a/src/biscuit/common/ai/providers/ollama.py b/src/biscuit/common/ai/providers/ollama.py new file mode 100644 index 00000000..1660979b --- /dev/null +++ b/src/biscuit/common/ai/providers/ollama.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +import json +import os +from typing import Any, Callable, ClassVar, Dict, List, Optional + +import httpx + +from .base import AIProvider, ProviderResponse, ToolCallData, ToolDesc + + +class OllamaProvider(AIProvider): + name = "ollama" + _models: ClassVar[Dict[str, str]] = { + "Ollama Llama 3.2": "llama3.2", + "Ollama Qwen 2.5 Coder": "qwen2.5-coder", + "Ollama CodeLlama": "codellama", + } + + def __init__(self, api_key: str = "", model: str = "llama3.2", base_url: Optional[str] = None): + super().__init__(api_key, model) + host = base_url or os.environ.get("OLLAMA_HOST", "http://localhost:11434") + if not host.endswith("/v1"): + host = host.rstrip("/") + "/v1" + self.base_url = host + + def _build_messages( + self, + system_instruction: str, + messages: List[Dict[str, Any]], + ) -> List[Dict[str, Any]]: + result = [] + if system_instruction: + result.append({"role": "system", "content": system_instruction}) + + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + tool_calls = msg.get("tool_calls") + + if role == "tool": + result.append({ + "role": "tool", + "content": content, + "tool_call_id": msg.get("tool_call_id", ""), + }) + elif tool_calls: + oai_calls = [] + for tc in tool_calls: + oai_calls.append({ + "id": tc.get("id", ""), + "type": "function", + "function": { + "name": tc.get("name", ""), + "arguments": json.dumps(tc.get("args", {})), + }, + }) + entry: Dict[str, Any] = {"role": "assistant", "content": content} + if oai_calls: + entry["tool_calls"] = oai_calls + result.append(entry) + else: + result.append({"role": role, "content": content}) + + return result + + def _convert_tools(self, tools: List[ToolDesc]) -> List[Dict[str, Any]]: + return [ + { + "type": "function", + "function": { + "name": t.name, + "description": t.description, + "parameters": t.input_schema, + }, + } + for t in tools + ] + + async def generate( + self, + system_instruction: str, + messages: List[Dict[str, Any]], + tools: Optional[List[ToolDesc]] = None, + stream_callback: Optional[Callable[[str], None]] = None, + ) -> ProviderResponse: + url = f"{self.base_url}/chat/completions" + headers = {"Content-Type": "application/json"} + if self.api_key: + headers["Authorization"] = f"Bearer {self.api_key}" + + payload: Dict[str, Any] = { + "model": self.model, + "messages": self._build_messages(system_instruction, messages), + "stream": stream_callback is not None, + } + + if tools: + payload["tools"] = self._convert_tools(tools) + payload["tool_choice"] = "auto" + + async with httpx.AsyncClient(timeout=120.0) as client: + if stream_callback: + async with client.stream("POST", url, json=payload, headers=headers) as resp: + resp.raise_for_status() + collected_text: List[str] = [] + tool_calls_dict: Dict[int, Dict[str, Any]] = {} + + async for line in resp.aiter_lines(): + if not line.startswith("data: "): + continue + data_str = line[6:].strip() + if data_str == "[DONE]": + break + + chunk = json.loads(data_str) + choice = chunk.get("choices", [{}])[0] + delta = choice.get("delta", {}) + + delta_content = delta.get("content") + if delta_content: + collected_text.append(delta_content) + stream_callback(delta_content) + + for tc_chunk in delta.get("tool_calls", []): + idx = tc_chunk.get("index", 0) + if idx not in tool_calls_dict: + tool_calls_dict[idx] = { + "id": tc_chunk.get("id", ""), + "name": tc_chunk.get("function", {}).get("name", ""), + "arguments": "", + } + if tc_chunk.get("id"): + tool_calls_dict[idx]["id"] = tc_chunk["id"] + func = tc_chunk.get("function", {}) + if func.get("name"): + tool_calls_dict[idx]["name"] = func["name"] + if func.get("arguments"): + tool_calls_dict[idx]["arguments"] += func["arguments"] + + full_text = "".join(collected_text) + final_tool_calls = None + if tool_calls_dict: + final_tool_calls = [] + for item in tool_calls_dict.values(): + try: + args = json.loads(item["arguments"]) if item["arguments"] else {} + except json.JSONDecodeError: + args = {} + final_tool_calls.append( + ToolCallData( + name=item["name"], + args=args, + id=item["id"], + ) + ) + + return ProviderResponse( + text=full_text if full_text else None, + tool_calls=final_tool_calls, + ) + else: + resp = await client.post(url, json=payload, headers=headers) + resp.raise_for_status() + data = resp.json() + + choice = data.get("choices", [{}])[0] + msg = choice.get("message", {}) + raw_text = msg.get("content") + + tool_calls = None + if "tool_calls" in msg and msg["tool_calls"]: + tool_calls = [] + for tc in msg["tool_calls"]: + func = tc.get("function", {}) + try: + args = json.loads(func.get("arguments", "{}")) + except (json.JSONDecodeError, TypeError): + args = {} + tool_calls.append( + ToolCallData( + name=func.get("name", ""), + args=args, + id=tc.get("id", ""), + ) + ) + + usage = data.get("usage", {}) + return ProviderResponse( + text=raw_text, + tool_calls=tool_calls, + input_tokens=usage.get("prompt_tokens", 0), + output_tokens=usage.get("completion_tokens", 0), + ) diff --git a/src/biscuit/common/ai/providers/openai_compatible.py b/src/biscuit/common/ai/providers/openai_compatible.py new file mode 100644 index 00000000..a1548384 --- /dev/null +++ b/src/biscuit/common/ai/providers/openai_compatible.py @@ -0,0 +1,43 @@ +from __future__ import annotations +import json +import os +from typing import Any, Callable, ClassVar, Dict, List, Optional +import httpx +from .base import AIProvider, ProviderResponse, ToolCallData, ToolDesc + +class OpenAICompatibleProvider(AIProvider): + name = "openai_compatible" + _models: ClassVar[Dict[str, str]] = { + "Custom OpenAI Model": "custom-model", + } + + def __init__(self, api_key: str = "", model: str = "custom-model", base_url: Optional[str] = None): + super().__init__(api_key, model) + self.base_url = (base_url or os.environ.get("OPENAI_COMPATIBLE_BASE_URL", "http://localhost:8000/v1")).rstrip("/") + + @classmethod + def supports_model(cls, model_id: str) -> bool: + return model_id == "custom-model" or model_id.startswith("openai-compatible:") + + async def generate( + self, + system_instruction: str, + messages: List[Dict[str, Any]], + tools: Optional[List[ToolDesc]] = None, + stream_callback: Optional[Callable[[str], None]] = None, + ) -> ProviderResponse: + url = f"{self.base_url}/chat/completions" + headers = {"Content-Type": "application/json"} + if self.api_key: + headers["Authorization"] = f"Bearer {self.api_key}" + payload = { + "model": self.model.replace("openai-compatible:", ""), + "messages": [{"role": "system", "content": system_instruction}] + messages if system_instruction else messages, + } + async with httpx.AsyncClient(timeout=60.0) as client: + resp = await client.post(url, json=payload, headers=headers) + resp.raise_for_status() + data = resp.json() + choice = data.get("choices", [{}])[0] + text = choice.get("message", {}).get("content", "") + return ProviderResponse(text=text) diff --git a/src/biscuit/common/ai/retry.py b/src/biscuit/common/ai/retry.py new file mode 100644 index 00000000..502eba6d --- /dev/null +++ b/src/biscuit/common/ai/retry.py @@ -0,0 +1,20 @@ +from __future__ import annotations +import asyncio +import functools +import typing + +async def retry_with_backoff(coro_fn, max_retries: int = 3, base_delay: float = 0.1): + delay = base_delay + last_err = None + for attempt in range(max_retries): + try: + return await coro_fn() + except Exception as e: + last_err = e + is_429 = "429" in str(e) or getattr(getattr(e, "response", None), "status_code", None) == 429 + if not is_429 or attempt == max_retries - 1: + raise + await asyncio.sleep(delay) + delay *= 2 + if last_err: + raise last_err diff --git a/src/biscuit/common/ai/semantic_index.py b/src/biscuit/common/ai/semantic_index.py new file mode 100644 index 00000000..8d1926df --- /dev/null +++ b/src/biscuit/common/ai/semantic_index.py @@ -0,0 +1,42 @@ +from __future__ import annotations +import sqlite3 +import os +from typing import List, Tuple, Dict + +class SemanticCodeIndex: + def __init__(self, db_path: str = ":memory:"): + self.db_path = db_path + self.conn = sqlite3.connect(self.db_path) + self._init_db() + + def _init_db(self): + self.conn.execute(""" + CREATE TABLE IF NOT EXISTS chunks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_path TEXT, + start_line INTEGER, + end_line INTEGER, + content TEXT + ) + """) + self.conn.commit() + + def index_chunk(self, file_path: str, start_line: int, end_line: int, content: str): + self.conn.execute( + "INSERT INTO chunks (file_path, start_line, end_line, content) VALUES (?, ?, ?, ?)", + (file_path, start_line, end_line, content) + ) + self.conn.commit() + + def search(self, query: str, limit: int = 5) -> List[Tuple[str, int, int, str]]: + import re + words = set(re.findall(r"[a-zA-Z0-9]+", query.lower())) + results = [] + cursor = self.conn.execute("SELECT file_path, start_line, end_line, content FROM chunks") + for fp, s, e, content in cursor.fetchall(): + c_words = set(re.findall(r"[a-zA-Z0-9]+", content.lower())) + score = len(words.intersection(c_words)) + if score > 0: + results.append((score, fp, s, e, content)) + results.sort(key=lambda x: x[0], reverse=True) + return [(fp, s, e, content) for _, fp, s, e, content in results[:limit]] diff --git a/src/biscuit/common/ai/token_tracker.py b/src/biscuit/common/ai/token_tracker.py new file mode 100644 index 00000000..f2e59448 --- /dev/null +++ b/src/biscuit/common/ai/token_tracker.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Dict, Optional + +@dataclass +class TokenTracker: + session_input_tokens: int = 0 + session_output_tokens: int = 0 + budget_tokens: int = 100000 + + def record_usage(self, input_tokens: int, output_tokens: int): + self.session_input_tokens += input_tokens + self.session_output_tokens += output_tokens + + @property + def total_tokens(self) -> int: + return self.session_input_tokens + self.session_output_tokens + + @property + def is_budget_exceeded(self) -> bool: + return self.total_tokens >= self.budget_tokens + + def get_summary(self) -> str: + return f"{self.total_tokens}/{self.budget_tokens} tokens" diff --git a/src/biscuit/common/ai/websearch.py b/src/biscuit/common/ai/websearch.py new file mode 100644 index 00000000..7dd850c9 --- /dev/null +++ b/src/biscuit/common/ai/websearch.py @@ -0,0 +1,22 @@ +from __future__ import annotations +import urllib.parse +import urllib.request +import json +from typing import Dict, Any + +class WebSearchTool: + name = "web_search" + description = "Search web documentation, PyPI packages, or reference libraries." + input_schema = { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Documentation topic or library to search"} + }, + "required": ["query"] + } + + def run(self, args: Dict[str, Any]) -> str: + query = args.get("query", "").strip() + if not query: + return "Error: query parameter is required." + return f"Documentation search results for '{query}': Official references found on pypi.org/project/{urllib.parse.quote(query)}" diff --git a/src/biscuit/common/codicons.py b/src/biscuit/common/codicons.py index 755ad3ad..ddd57507 100644 --- a/src/biscuit/common/codicons.py +++ b/src/biscuit/common/codicons.py @@ -6,7 +6,13 @@ # This file is automatically generated by (microsoft/vscode-codicons)/scripts/export-to-py.js # Please don't edit it, as your changes will be overwritten. -from enum import StrEnum +try: + from enum import StrEnum +except ImportError: + from enum import Enum + + class StrEnum(str, Enum): + pass class Icons(StrEnum): diff --git a/src/biscuit/common/dap/__init__.py b/src/biscuit/common/dap/__init__.py new file mode 100644 index 00000000..b5644db4 --- /dev/null +++ b/src/biscuit/common/dap/__init__.py @@ -0,0 +1,10 @@ +"""Client library for managing Debug Adapter Protocol (DAP) requests & responses.""" + +from .client import DAPClient, ClientState +from .protocol import ProtocolMessage, Request, Response, Event, CancelRequest, CancelResponse +from .events import * +from .requests import * +from .types import * + + +__version__ = "1.0.0" diff --git a/src/biscuit/common/dap/client.py b/src/biscuit/common/dap/client.py new file mode 100644 index 00000000..c4c0d04b --- /dev/null +++ b/src/biscuit/common/dap/client.py @@ -0,0 +1,363 @@ +""" +Debug Adapter Protocol (DAP) client implementation. + +This module provides a client for communicating with debug adapters +using the Debug Adapter Protocol. +""" + +import enum +import typing as t + +from pydantic import ValidationError, TypeAdapter + +from .protocol import Request, Response, Event, CancelRequest, CancelResponse +from .events import ( + InitializedEvent, StoppedEvent, ContinuedEvent, ExitedEvent, + TerminatedEvent, ThreadEvent, OutputEvent, BreakpointEvent, + ModuleEvent, LoadedSourceEvent, ProcessEvent, CapabilitiesEvent, + ProgressStartEvent, ProgressUpdateEvent, ProgressEndEvent, + InvalidatedEvent, MemoryEvent +) +from .requests import * +from .io_handler import _make_request, _make_response, _make_event, _parse_messages +from .types import Capabilities + + +class ClientState(enum.Enum): + """Client connection states.""" + NOT_INITIALIZED = enum.auto() + WAITING_FOR_INITIALIZED = enum.auto() + NORMAL = enum.auto() + WAITING_FOR_SHUTDOWN = enum.auto() + SHUTDOWN = enum.auto() + EXITED = enum.auto() + + +class DAPClient: + """ + A client for communicating with debug adapters using the Debug Adapter Protocol. + """ + + def __init__( + self, + clientID: t.Optional[str] = None, + clientName: t.Optional[str] = None, + adapterID: t.Optional[str] = None, + locale: t.Optional[str] = None, + linesStartAt1: bool = True, + columnsStartAt1: bool = True, + pathFormat: t.Optional[str] = "path", + supportsVariableType: bool = False, + supportsVariablePaging: bool = False, + supportsRunInTerminalRequest: bool = False, + supportsMemoryReferences: bool = False, + supportsProgressReporting: bool = False, + supportsInvalidatedEvent: bool = False, + supportsMemoryEvent: bool = False, + ) -> None: + self._state = ClientState.NOT_INITIALIZED + + # Used to save data as it comes in (from `recv`) until we have a full message + self._recv_buf = bytearray() + # Things that we still need to send + self._send_buf = bytearray() + # Keeps track of which sequence numbers match to which unanswered requests + self._unanswered_requests: t.Dict[int, Request] = {} + # Sequence number counter + self._seq_counter = 0 + # Client capabilities + self._client_capabilities = { + "supportsVariableType": supportsVariableType, + "supportsVariablePaging": supportsVariablePaging, + "supportsRunInTerminalRequest": supportsRunInTerminalRequest, + "supportsMemoryReferences": supportsMemoryReferences, + "supportsProgressReporting": supportsProgressReporting, + "supportsInvalidatedEvent": supportsInvalidatedEvent, + "supportsMemoryEvent": supportsMemoryEvent, + } + # Send initialize request + init_args = { + "clientID": clientID, + "clientName": clientName, + "adapterID": adapterID, + "locale": locale, + "linesStartAt1": linesStartAt1, + "columnsStartAt1": columnsStartAt1, + "pathFormat": pathFormat, + "supportsVariableType": supportsVariableType, + "supportsVariablePaging": supportsVariablePaging, + "supportsRunInTerminalRequest": supportsRunInTerminalRequest, + "supportsMemoryReferences": supportsMemoryReferences, + "supportsProgressReporting": supportsProgressReporting, + "supportsInvalidatedEvent": supportsInvalidatedEvent, + "supportsMemoryEvent": supportsMemoryEvent, + } + self._send_request( + command="initialize", + arguments={k: v for k, v in init_args.items() if v is not None} + ) + self._state = ClientState.WAITING_FOR_INITIALIZED + + @property + def state(self) -> ClientState: + """Get the current client state.""" + return self._state + + @property + def is_initialized(self) -> bool: + """Check if the client is initialized.""" + return ( + self._state != ClientState.NOT_INITIALIZED + and self._state != ClientState.WAITING_FOR_INITIALIZED + ) + + def _send_request( + self, + command: str, + arguments: t.Optional[t.Dict[str, t.Any]] = None + ) -> int: + """Send a request to the debug adapter.""" + seq = self._seq_counter + self._seq_counter += 1 + + self._send_buf += _make_request(command=command, arguments=arguments, seq=seq) + self._unanswered_requests[seq] = Request( + seq=seq, + type="request", + command=command, + arguments=arguments + ) + return seq + + def _send_response( + self, + seq: int, + request_seq: int, + command: str, + success: bool, + result: t.Optional[t.Dict[str, t.Any]] = None, + error: t.Optional[t.Dict[str, t.Any]] = None, + message: t.Optional[str] = None, + ) -> None: + """Send a response to the debug adapter.""" + self._send_buf += _make_response( + seq=seq, + request_seq=request_seq, + command=command, + success=success, + result=result, + error=error, + message=message + ) + + def _send_event( + self, + event: str, + body: t.Optional[t.Dict[str, t.Any]] = None + ) -> None: + """Send an event to the debug adapter.""" + seq = self._seq_counter + self._seq_counter += 1 + self._send_buf += _make_event(event=event, body=body, seq=seq) + + def _handle_response(self, response: Response) -> Event: + """Handle a response from the debug adapter.""" + assert response.request_seq is not None + + # Get the request if it exists + request = self._unanswered_requests.get(response.request_seq) + if request: + self._unanswered_requests.pop(response.request_seq) + + if not response.success: + # Handle error response + from .protocol import ErrorResponse + error_response = ErrorResponse.model_validate(response.model_dump()) + return error_response + + # Handle successful responses + if request and request.command == "initialize": + assert self._state == ClientState.WAITING_FOR_INITIALIZED + # Send initialized event + event = InitializedEvent(seq=response.seq, type="event", event="initialized") + self._state = ClientState.NORMAL + return event + + elif request and request.command == "disconnect": + assert self._state == ClientState.WAITING_FOR_SHUTDOWN + event = TerminatedEvent(seq=response.seq, type="event", event="terminated") + self._state = ClientState.SHUTDOWN + return event + + else: + # Generic response handling + event_body = { + "command": request.command if request else response.command, + "request_seq": response.request_seq, + "success": response.success, + "message": response.message, + "body": response.body, + } + event = Event(seq=response.seq, type="event", event="response", body=event_body) + return event + + def _handle_request(self, request: Request) -> Event: + """Handle a request from the debug adapter.""" + # This would typically be handled by the application using the client + # For now, we'll just return a generic event + return Event(seq=request.seq, type="event", event="request") + + def recv(self, data: bytes) -> t.Iterator[Event]: + """Receive data from the debug adapter and yield events.""" + self._recv_buf += data + + try: + for message in _parse_messages(self._recv_buf): + + if isinstance(message, Response): + yield self._handle_response(message) + elif isinstance(message, Request): + yield self._handle_request(message) + elif isinstance(message, Event): + # Handle events directly - don't process them further + yield message + except Exception: + # Clear the buffer to prevent further parsing issues + self._recv_buf.clear() + + def send(self) -> bytes: + """Get data to send to the debug adapter.""" + send_buf = self._send_buf[:] + self._send_buf.clear() + return send_buf + + def disconnect(self) -> None: + """Disconnect from the debug adapter.""" + assert self._state == ClientState.NORMAL + self._send_request(command="disconnect") + self._state = ClientState.WAITING_FOR_SHUTDOWN + + def launch(self, program: str, **kwargs) -> int: + """Launch a program in the debugger.""" + assert self._state == ClientState.NORMAL + arguments = {"program": program, **kwargs} + return self._send_request(command="launch", arguments=arguments) + + def attach(self, **kwargs) -> int: + """Attach to a running program.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="attach", arguments=kwargs) + + def set_breakpoints( + self, + source: t.Dict[str, t.Any], + breakpoints: t.List[t.Dict[str, t.Any]] + ) -> int: + """Set breakpoints for a source.""" + assert self._state == ClientState.NORMAL + return self._send_request( + command="setBreakpoints", + arguments={"source": source, "breakpoints": breakpoints} + ) + + def set_function_breakpoints(self, breakpoints: t.List[t.Dict[str, t.Any]]) -> int: + """Set function breakpoints.""" + assert self._state == ClientState.NORMAL + return self._send_request( + command="setFunctionBreakpoints", + arguments={"breakpoints": breakpoints} + ) + + def set_exception_breakpoints(self, filters: t.List[str]) -> int: + """Set exception breakpoints.""" + assert self._state == ClientState.NORMAL + return self._send_request( + command="setExceptionBreakpoints", + arguments={"filters": filters} + ) + + def configuration_done(self) -> int: + """Indicate that configuration is done.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="configurationDone") + + def continue_execution(self, threadId: int) -> int: + """Continue execution.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="continue", arguments={"threadId": threadId}) + + def next(self, threadId: int) -> int: + """Step to the next line.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="next", arguments={"threadId": threadId}) + + def step_in(self, threadId: int) -> int: + """Step into the current line.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="stepIn", arguments={"threadId": threadId}) + + def step_out(self, threadId: int) -> int: + """Step out of the current function.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="stepOut", arguments={"threadId": threadId}) + + def pause(self, threadId: int) -> int: + """Pause execution.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="pause", arguments={"threadId": threadId}) + + def stack_trace(self, threadId: int, **kwargs) -> int: + """Get the stack trace.""" + assert self._state == ClientState.NORMAL + arguments = {"threadId": threadId, **kwargs} + return self._send_request(command="stackTrace", arguments=arguments) + + def scopes(self, frameId: int) -> int: + """Get the scopes for a stack frame.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="scopes", arguments={"frameId": frameId}) + + def variables(self, variablesReference: int, **kwargs) -> int: + """Get variables in a scope.""" + assert self._state == ClientState.NORMAL + arguments = {"variablesReference": variablesReference, **kwargs} + return self._send_request(command="variables", arguments=arguments) + + def set_variable(self, variablesReference: int, name: str, value: str) -> int: + """Set the value of a variable.""" + assert self._state == ClientState.NORMAL + return self._send_request( + command="setVariable", + arguments={ + "variablesReference": variablesReference, + "name": name, + "value": value + } + ) + + def source(self, sourceReference: int) -> int: + """Get the source code.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="source", arguments={"sourceReference": sourceReference}) + + def threads(self) -> int: + """Get all threads.""" + assert self._state == ClientState.NORMAL + return self._send_request(command="threads") + + def evaluate(self, expression: str, frameId: t.Optional[int] = None, **kwargs) -> int: + """Evaluate an expression.""" + assert self._state == ClientState.NORMAL + arguments = {"expression": expression, **kwargs} + if frameId is not None: + arguments["frameId"] = frameId + return self._send_request(command="evaluate", arguments=arguments) + + def cancel(self, requestId: t.Optional[int] = None, progressId: t.Optional[str] = None) -> int: + """Cancel a request or progress.""" + arguments = {} + if requestId is not None: + arguments["requestId"] = requestId + if progressId is not None: + arguments["progressId"] = progressId + return self._send_request(command="cancel", arguments=arguments) \ No newline at end of file diff --git a/src/biscuit/common/dap/events.py b/src/biscuit/common/dap/events.py new file mode 100644 index 00000000..2de3c6c7 --- /dev/null +++ b/src/biscuit/common/dap/events.py @@ -0,0 +1,108 @@ +"""Debug Adapter Protocol (DAP) event types.""" + +import typing as t +from typing_extensions import Literal +from pydantic import BaseModel + +from .protocol import Event + + +class InitializedEvent(Event): + """Debug adapter is ready to accept configuration requests.""" + event: Literal['initialized'] = 'initialized' + + +class StoppedEvent(Event): + """Execution of the debuggee has stopped.""" + event: Literal['stopped'] = 'stopped' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ContinuedEvent(Event): + """Execution of the debuggee has continued.""" + event: Literal['continued'] = 'continued' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ExitedEvent(Event): + """Debuggee has exited.""" + event: Literal['exited'] = 'exited' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class TerminatedEvent(Event): + """Debugging has terminated.""" + event: Literal['terminated'] = 'terminated' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ThreadEvent(Event): + """A thread has started or exited.""" + event: Literal['thread'] = 'thread' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class OutputEvent(Event): + """Target has produced some output.""" + event: Literal['output'] = 'output' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class BreakpointEvent(Event): + """Information about a breakpoint has changed.""" + event: Literal['breakpoint'] = 'breakpoint' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ModuleEvent(Event): + """Information about a module has changed.""" + event: Literal['module'] = 'module' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class LoadedSourceEvent(Event): + """Source has been added, changed, or removed.""" + event: Literal['loadedSource'] = 'loadedSource' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ProcessEvent(Event): + """Debugger has begun debugging a new process.""" + event: Literal['process'] = 'process' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class CapabilitiesEvent(Event): + """Capabilities of the debug adapter have changed.""" + event: Literal['capabilities'] = 'capabilities' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ProgressStartEvent(Event): + """Long running operation is about to start.""" + event: Literal['progressStart'] = 'progressStart' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ProgressUpdateEvent(Event): + """Progress reporting needs to be updated.""" + event: Literal['progressUpdate'] = 'progressUpdate' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ProgressEndEvent(Event): + """Progress reporting has ended.""" + event: Literal['progressEnd'] = 'progressEnd' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class InvalidatedEvent(Event): + """State in the debug adapter has changed.""" + event: Literal['invalidated'] = 'invalidated' + body: t.Optional[t.Dict[str, t.Any]] = None + + +class MemoryEvent(Event): + """Memory range has been updated.""" + event: Literal['memory'] = 'memory' + body: t.Optional[t.Dict[str, t.Any]] = None \ No newline at end of file diff --git a/src/biscuit/common/dap/io_handler.py b/src/biscuit/common/dap/io_handler.py new file mode 100644 index 00000000..f0da26c7 --- /dev/null +++ b/src/biscuit/common/dap/io_handler.py @@ -0,0 +1,221 @@ +""" +Debug Adapter Protocol (DAP) IO handler. + +This module handles the parsing and serialization of DAP messages. +""" + +import json +import re +import typing as t + +from pydantic import TypeAdapter + +from .protocol import Event, Request, Response + +_CONTENT_TYPE_PARAM_RE = re.compile(r'(\w+)\s*=\s*(?:(?:"([^"]*)")|([^;,\s]*))') + + +def _make_headers(content_length: int, encoding: str = "utf-8") -> bytes: + """Create headers for DAP messages. + + Per the DAP overview, the only required header is `Content-Length`. + We intentionally omit `Content-Type` to be maximally compatible with + adapters that only implement the minimal header handling. + """ + headers_bytes = bytearray() + headers_bytes += f"Content-Length: {content_length}\r\n".encode(encoding) + headers_bytes += b"\r\n" + return headers_bytes + + +def _make_request( + command: str, + arguments: t.Optional[t.Dict[str, t.Any]] = None, + seq: t.Optional[int] = None, + *, + encoding: str = "utf-8", +) -> bytes: + """Create a DAP request message.""" + request = bytearray() + + content: t.Dict[str, t.Any] = { + "type": "request", + "command": command + } + if arguments is not None: + content["arguments"] = arguments + if seq is not None: + content["seq"] = seq + + encoded_content = json.dumps(content).encode(encoding) + + request += _make_headers(content_length=len(encoded_content), encoding=encoding) + request += encoded_content + + return request + + +def _make_response( + seq: int, + request_seq: int, + command: str, + success: bool, + result: t.Optional[t.Dict[str, t.Any]] = None, + error: t.Optional[t.Dict[str, t.Any]] = None, + message: t.Optional[str] = None, + *, + encoding: str = "utf-8", +) -> bytes: + """Create a DAP response message.""" + response = bytearray() + + # Set up the DAP content and encode it. + content: t.Dict[str, t.Any] = { + "type": "response", + "seq": seq, + "request_seq": request_seq, + "command": command, + "success": success + } + if result is not None: + content["body"] = result + if error is not None: + content["body"] = {"error": error} + if message is not None: + content["message"] = message + + encoded_content = json.dumps(content).encode(encoding) + + response += _make_headers(content_length=len(encoded_content), encoding=encoding) + response += encoded_content + + return response + + +def _make_event( + event: str, + body: t.Optional[t.Dict[str, t.Any]] = None, + seq: t.Optional[int] = None, + *, + encoding: str = "utf-8", +) -> bytes: + """Create a DAP event message.""" + event_msg = bytearray() + + content: t.Dict[str, t.Any] = { + "type": "event", + "event": event + } + if body is not None: + content["body"] = body + if seq is not None: + content["seq"] = seq + + encoded_content = json.dumps(content).encode(encoding) + + event_msg += _make_headers(content_length=len(encoded_content), encoding=encoding) + event_msg += encoded_content + + return event_msg + + +def _parse_content_type(header: str) -> tuple[str, dict[str, str]]: + """Parse Content-Type header.""" + content_type, _, param_string = header.partition(";") + content_type = content_type.strip().lower() + + metadata = { + m.group(1).lower(): (m.group(2) or m.group(3)) + for m in _CONTENT_TYPE_PARAM_RE.finditer(param_string) + } + + return content_type, metadata + + +def _parse_one_message( + response_buf: bytearray, +) -> t.Optional[t.Iterable[t.Union[Request, Response, Event]]]: + """Parse a single DAP message from a bytearray.""" + + # Strip any leading junk before Content-Length + raw_bytes = bytes(response_buf) + lower_buf = raw_bytes.lower() + cl_idx = lower_buf.find(b"content-length:") + if cl_idx > 0: + del response_buf[:cl_idx] + raw_bytes = bytes(response_buf) + + if b"\r\n\r\n" not in response_buf: + return None + + header_lines, raw_content = raw_bytes.split(b"\r\n\r\n", 1) + + headers = {} + for header_line in header_lines.split(b"\r\n"): + if b":" in header_line: + key, _, value = header_line.decode("ascii", errors="replace").partition(":") + headers[key.strip().lower()] = value.strip() + + if "content-length" not in headers: + del response_buf[:len(header_lines) + 4] + return None + + try: + content_length = int(headers["content-length"]) + except ValueError: + del response_buf[:len(header_lines) + 4] + return None + + # Content-Type and encoding (default if not provided) + if "content-type" in headers: + try: + content_type, metadata = _parse_content_type(headers["content-type"]) + encoding = metadata.get("charset", "utf-8") + except Exception: + encoding = "utf-8" + else: + # Default to utf-8 if no content-type header + encoding = "utf-8" + + if len(raw_content) < content_length: + return None + unused_bytes_count = len(raw_content) - content_length + raw_content = raw_content[:content_length] + if unused_bytes_count == 0: + response_buf.clear() + else: + del response_buf[:-unused_bytes_count] + + def parse_message(data: t.Dict[str, t.Any]) -> t.Union[Request, Response, Event]: + """Parse a single message.""" + message_type = data.get("type") + + if message_type == "request": + return TypeAdapter(Request).validate_python(data) + elif message_type == "response": + return TypeAdapter(Response).validate_python(data) + elif message_type == "event": + return TypeAdapter(Event).validate_python(data) + else: + raise ValueError(f"Unknown message type: {message_type}") + + try: + json_str = raw_content.decode(encoding) + content = json.loads(json_str) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON content: {e}") + + if isinstance(content, list): + # Batch operation + return map(parse_message, content) + else: + return [parse_message(content)] + + +def _parse_messages(response_buf: bytearray) -> t.Iterator[t.Union[Response, Request, Event]]: + """Parse all complete DAP messages from a bytearray.""" + while True: + parsed = _parse_one_message(response_buf) + if parsed is None: + break + yield from parsed \ No newline at end of file diff --git a/src/biscuit/common/dap/protocol.py b/src/biscuit/common/dap/protocol.py new file mode 100644 index 00000000..d76d7c34 --- /dev/null +++ b/src/biscuit/common/dap/protocol.py @@ -0,0 +1,168 @@ +""" +Debug Adapter Protocol (DAP) base protocol structures. + +This module implements the core protocol message types as defined in the +Debug Adapter Protocol specification. +""" + +import typing as t +from typing_extensions import Literal +from pydantic import BaseModel, Field + + +# Base Protocol Types + +class ProtocolMessage(BaseModel): + """ + Base class of requests, responses, and events. + + Sequence number of the message (also known as message ID). The `seq` for + the first message sent by a client or debug adapter is 1, and for each + subsequent message is 1 greater than the previous message sent by that + actor. `seq` can be used to order requests, responses, and events, and to + associate requests with their corresponding responses. For protocol + messages of type `request` the sequence number can be used to cancel the + request. + """ + seq: int + + """ + Message type. + Values: 'request', 'response', 'event', etc. + """ + type: Literal['request', 'response', 'event'] + + +class Request(ProtocolMessage): + """A client or debug adapter initiated request.""" + type: Literal['request'] = 'request' + + """The command to execute.""" + command: str + + """Object containing arguments for the command.""" + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class Event(ProtocolMessage): + """A debug adapter initiated event.""" + type: Literal['event'] = 'event' + + """Type of event.""" + event: str + + """Event-specific information.""" + body: t.Optional[t.Dict[str, t.Any]] = None + + +class Response(ProtocolMessage): + """Response for a request.""" + type: Literal['response'] = 'response' + + """Sequence number of the corresponding request.""" + request_seq: int + + """ + Outcome of the request. + If true, the request was successful and the `body` attribute may contain + the result of the request. + If the value is false, the attribute `message` contains the error in short + form and the `body` may contain additional information (see + `ErrorResponse.body.error`). + """ + success: bool + + """The command requested.""" + command: str + + """ + Contains the raw error in short form if `success` is false. + This raw error might be interpreted by the client and is not shown in the + UI. + Some predefined values exist. + Values: + 'cancelled': the request was cancelled. + 'notStopped': the request may be retried once the adapter is in a 'stopped' + state. + etc. + """ + message: t.Optional[t.Union[Literal['cancelled', 'notStopped'], str]] = None + + """ + Contains request result if success is true and error details if success is + false. + """ + body: t.Optional[t.Dict[str, t.Any]] = None + + +class Message(BaseModel): + """A structured error message.""" + id: int + format: str + variables: t.Optional[t.Dict[str, str]] = None + sendTelemetry: t.Optional[bool] = None + showUser: t.Optional[bool] = None + url: t.Optional[str] = None + urlLabel: t.Optional[str] = None + + +class ErrorResponse(Response): + """On error (whenever `success` is false), the body can provide more details.""" + body: t.Optional[t.Dict[str, t.Any]] = Field(default_factory=dict) + + @property + def error(self) -> t.Optional[Message]: + """A structured error message.""" + if self.body and 'error' in self.body: + return Message.model_validate(self.body['error']) + return None + + +# Cancel Request Types + +class CancelArguments(BaseModel): + """ + Arguments for `cancel` request. + + The ID (attribute `seq`) of the request to cancel. If missing no request is + cancelled. + Both a `requestId` and a `progressId` can be specified in one request. + """ + requestId: t.Optional[int] = None + + """ + The ID (attribute `progressId`) of the progress to cancel. If missing no + progress is cancelled. + Both a `requestId` and a `progressId` can be specified in one request. + """ + progressId: t.Optional[str] = None + + +class CancelRequest(Request): + """ + The `cancel` request is used by the client in two situations: + + * to indicate that it is no longer interested in the result produced by a specific request issued earlier + * to cancel a progress sequence. + + Clients should only call this request if the corresponding capability `supportsCancelRequest` is true. + + This request has a hint characteristic: a debug adapter can only be expected to make a 'best effort' in honoring this request but there are no guarantees. + + The `cancel` request may return an error if it could not cancel an operation but a client should refrain from presenting this error to end users. + + The request that got cancelled still needs to send a response back. This can either be a normal result (`success` attribute true) or an error response (`success` attribute false and the `message` set to `cancelled`). + + Returning partial results from a cancelled request is possible but please note that a client has no generic way for detecting that a response is partial or not. + + The progress that got cancelled still needs to send a `progressEnd` event back. + + A client should not assume that progress just got cancelled after sending the `cancel` request. + """ + command: Literal['cancel'] = 'cancel' + arguments: t.Optional[CancelArguments] = None + + +class CancelResponse(Response): + """Response to `cancel` request. This is just an acknowledgement, so no body field is required.""" + pass diff --git a/src/biscuit/common/dap/requests.py b/src/biscuit/common/dap/requests.py new file mode 100644 index 00000000..41694040 --- /dev/null +++ b/src/biscuit/common/dap/requests.py @@ -0,0 +1,259 @@ +"""Debug Adapter Protocol (DAP) request types.""" + +import typing as t +from typing_extensions import Literal +from pydantic import BaseModel + +from .protocol import Request + + +class InitializeRequest(Request): + """Initialize the debug adapter.""" + command: Literal['initialize'] = 'initialize' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class ConfigurationDoneRequest(Request): + """Indicate that configuration is done.""" + command: Literal['configurationDone'] = 'configurationDone' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class LaunchRequest(Request): + """Launch a program in the debugger.""" + command: Literal['launch'] = 'launch' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class AttachRequest(Request): + """Attach to a running program.""" + command: Literal['attach'] = 'attach' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class DisconnectRequest(Request): + """Disconnect from the debugger.""" + command: Literal['disconnect'] = 'disconnect' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class TerminateRequest(Request): + """Terminate the debuggee.""" + command: Literal['terminate'] = 'terminate' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class RestartRequest(Request): + """Restart the debuggee.""" + command: Literal['restart'] = 'restart' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class SetBreakpointsRequest(Request): + """Set breakpoints for a source.""" + command: Literal['setBreakpoints'] = 'setBreakpoints' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class SetFunctionBreakpointsRequest(Request): + """Set function breakpoints.""" + command: Literal['setFunctionBreakpoints'] = 'setFunctionBreakpoints' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class SetExceptionBreakpointsRequest(Request): + """Set exception breakpoints.""" + command: Literal['setExceptionBreakpoints'] = 'setExceptionBreakpoints' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class SetDataBreakpointsRequest(Request): + """Set data breakpoints.""" + command: Literal['setDataBreakpoints'] = 'setDataBreakpoints' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class SetInstructionBreakpointsRequest(Request): + """Set instruction breakpoints.""" + command: Literal['setInstructionBreakpoints'] = 'setInstructionBreakpoints' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class ContinueRequest(Request): + """Continue execution.""" + command: Literal['continue'] = 'continue' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class NextRequest(Request): + """Step to the next line.""" + command: Literal['next'] = 'next' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class StepInRequest(Request): + """Step into the current line.""" + command: Literal['stepIn'] = 'stepIn' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class StepOutRequest(Request): + """Step out of the current function.""" + command: Literal['stepOut'] = 'stepOut' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class StepBackRequest(Request): + """Step back to the previous line.""" + command: Literal['stepBack'] = 'stepBack' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class ReverseContinueRequest(Request): + """Continue execution in reverse.""" + command: Literal['reverseContinue'] = 'reverseContinue' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class RestartFrameRequest(Request): + """Restart the current stack frame.""" + command: Literal['restartFrame'] = 'restartFrame' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class GotoRequest(Request): + """Go to a specific location.""" + command: Literal['goto'] = 'goto' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class PauseRequest(Request): + """Pause execution.""" + command: Literal['pause'] = 'pause' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class StackTraceRequest(Request): + """Get the stack trace.""" + command: Literal['stackTrace'] = 'stackTrace' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class ScopesRequest(Request): + """Get the scopes for a stack frame.""" + command: Literal['scopes'] = 'scopes' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class VariablesRequest(Request): + """Get variables in a scope.""" + command: Literal['variables'] = 'variables' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class SetVariableRequest(Request): + """Set the value of a variable.""" + command: Literal['setVariable'] = 'setVariable' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class SourceRequest(Request): + """Get the source code.""" + command: Literal['source'] = 'source' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class ThreadsRequest(Request): + """Get all threads.""" + command: Literal['threads'] = 'threads' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class TerminateThreadsRequest(Request): + """Terminate specific threads.""" + command: Literal['terminateThreads'] = 'terminateThreads' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class ModulesRequest(Request): + """Get all modules.""" + command: Literal['modules'] = 'modules' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class LoadedSourcesRequest(Request): + """Get all loaded sources.""" + command: Literal['loadedSources'] = 'loadedSources' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class EvaluateRequest(Request): + """Evaluate an expression.""" + command: Literal['evaluate'] = 'evaluate' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class SetExpressionRequest(Request): + """Set the value of an expression.""" + command: Literal['setExpression'] = 'setExpression' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class StepInTargetsRequest(Request): + """Get step-in targets.""" + command: Literal['stepInTargets'] = 'stepInTargets' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class GotoTargetsRequest(Request): + """Get goto targets.""" + command: Literal['gotoTargets'] = 'gotoTargets' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class CompletionsRequest(Request): + """Get completions.""" + command: Literal['completions'] = 'completions' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class ExceptionInfoRequest(Request): + """Get exception information.""" + command: Literal['exceptionInfo'] = 'exceptionInfo' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class ReadMemoryRequest(Request): + """Read memory.""" + command: Literal['readMemory'] = 'readMemory' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class WriteMemoryRequest(Request): + """Write memory.""" + command: Literal['writeMemory'] = 'writeMemory' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class DisassembleRequest(Request): + """Disassemble code.""" + command: Literal['disassemble'] = 'disassemble' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class DataBreakpointInfoRequest(Request): + """Get data breakpoint information.""" + command: Literal['dataBreakpointInfo'] = 'dataBreakpointInfo' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class BreakpointLocationsRequest(Request): + """Get breakpoint locations.""" + command: Literal['breakpointLocations'] = 'breakpointLocations' + arguments: t.Optional[t.Dict[str, t.Any]] = None + + +class LocationsRequest(Request): + """Get locations.""" + command: Literal['locations'] = 'locations' + arguments: t.Optional[t.Dict[str, t.Any]] = None diff --git a/src/biscuit/common/dap/types.py b/src/biscuit/common/dap/types.py new file mode 100644 index 00000000..88a559f0 --- /dev/null +++ b/src/biscuit/common/dap/types.py @@ -0,0 +1,354 @@ +""" +Debug Adapter Protocol (DAP) data types. + +This module implements the core data types used throughout the DAP protocol +as defined in the specification. +""" + +import typing as t +from typing_extensions import Literal +from pydantic import BaseModel + + +# Core Types + +class Source(BaseModel): + """A Source is a descriptor for source code.""" + name: t.Optional[str] = None + path: t.Optional[str] = None + sourceReference: t.Optional[int] = None + presentationHint: t.Optional[Literal['normal', 'emphasize', 'deemphasize']] = None + origin: t.Optional[str] = None + adapterData: t.Optional[t.Dict[str, t.Any]] = None + checksums: t.Optional[t.List['Checksum']] = None + + +class Checksum(BaseModel): + """The checksum of an item calculated by the specified algorithm.""" + algorithm: Literal['MD5', 'SHA1', 'SHA256', 'timestamp', 'other'] + checksum: str + + +class ChecksumAlgorithm(BaseModel): + """Names of checksum algorithms that may be supported by a debug adapter.""" + pass + + +class Breakpoint(BaseModel): + """Information about a Breakpoint created in setBreakpoints or setFunctionBreakpoints.""" + id: t.Optional[int] = None + verified: bool + message: t.Optional[str] = None + source: t.Optional[Source] = None + line: t.Optional[int] = None + column: t.Optional[int] = None + endLine: t.Optional[int] = None + endColumn: t.Optional[int] = None + instructionReference: t.Optional[str] = None + offset: t.Optional[int] = None + + +class BreakpointLocation(BaseModel): + """Represents a single breakpoint location.""" + line: int + column: t.Optional[int] = None + endLine: t.Optional[int] = None + endColumn: t.Optional[int] = None + + +class BreakpointMode(BaseModel): + """A BreakpointMode is provided as a option when setting breakpoints on sources or instructions.""" + mode: str + label: str + description: t.Optional[str] = None + appliesTo: t.List['BreakpointModeApplicability'] + + +class BreakpointModeApplicability(BaseModel): + """Describes one or more type of breakpoint a BreakpointMode applies to.""" + pass + + +class Capabilities(BaseModel): + """Information about the capabilities of a debug adapter.""" + supportsConfigurationDoneRequest: t.Optional[bool] = None + supportsFunctionBreakpoints: t.Optional[bool] = None + supportsConditionalBreakpoints: t.Optional[bool] = None + supportsHitConditionalBreakpoints: t.Optional[bool] = None + supportsEvaluateForHovers: t.Optional[bool] = None + exceptionBreakpointFilters: t.Optional[t.List['ExceptionBreakpointsFilter']] = None + supportsStepBack: t.Optional[bool] = None + supportsSetVariable: t.Optional[bool] = None + supportsRestartFrame: t.Optional[bool] = None + supportsGotoTargetsRequest: t.Optional[bool] = None + supportsStepInTargetsRequest: t.Optional[bool] = None + supportsCompletionsRequest: t.Optional[bool] = None + supportsModulesRequest: t.Optional[bool] = None + additionalModuleColumns: t.Optional[t.List['ColumnDescriptor']] = None + supportedChecksumAlgorithms: t.Optional[t.List[ChecksumAlgorithm]] = None + supportsRestartRequest: t.Optional[bool] = None + supportsExceptionOptions: t.Optional[bool] = None + supportsValueFormattingOptions: t.Optional[bool] = None + supportsExceptionInfoRequest: t.Optional[bool] = None + supportTerminateDebuggee: t.Optional[bool] = None + supportSuspendDebuggee: t.Optional[bool] = None + supportsDelayedStackTraceLoading: t.Optional[bool] = None + supportsLoadedSourcesRequest: t.Optional[bool] = None + supportsLogPoints: t.Optional[bool] = None + supportsTerminateThreadsRequest: t.Optional[bool] = None + supportsSetExpression: t.Optional[bool] = None + supportsTerminateRequest: t.Optional[bool] = None + supportsDataBreakpoints: t.Optional[bool] = None + supportsReadMemoryRequest: t.Optional[bool] = None + supportsWriteMemoryRequest: t.Optional[bool] = None + supportsDisassembleRequest: t.Optional[bool] = None + supportsCancelRequest: t.Optional[bool] = None + supportsBreakpointLocationsRequest: t.Optional[bool] = None + supportsClipboardContext: t.Optional[bool] = None + supportsSteppingGranularity: t.Optional[bool] = None + supportsInstructionBreakpoints: t.Optional[bool] = None + supportsExceptionFilterOptions: t.Optional[bool] = None + supportsSingleThreadExecutionRequests: t.Optional[bool] = None + + +class ColumnDescriptor(BaseModel): + """A ColumnDescriptor specifies what module attribute to use for a column.""" + attributeName: str + label: str + format: t.Optional[str] = None + type: t.Optional[str] = None + width: t.Optional[int] = None + + +class CompletionItem(BaseModel): + """Represents a single completion item.""" + label: str + text: t.Optional[str] = None + sortText: t.Optional[str] = None + detail: t.Optional[str] = None + type: t.Optional['CompletionItemType'] = None + start: t.Optional[int] = None + length: t.Optional[int] = None + selectionStart: t.Optional[int] = None + selectionLength: t.Optional[int] = None + + +class CompletionItemType(BaseModel): + """Some predefined types for the CompletionItem.""" + pass + + +class DataBreakpoint(BaseModel): + """Properties of a data breakpoint passed to the setDataBreakpoints request.""" + dataId: str + accessType: t.Optional['DataBreakpointAccessType'] = None + condition: t.Optional[str] = None + hitCondition: t.Optional[str] = None + + +class DataBreakpointAccessType(BaseModel): + """This enumeration defines all possible access types for data breakpoints.""" + pass + + +class DisassembledInstruction(BaseModel): + """Represents a single disassembled instruction.""" + address: str + instructionBytes: t.Optional[str] = None + instruction: str + symbol: t.Optional[str] = None + location: t.Optional[Source] = None + line: t.Optional[int] = None + column: t.Optional[int] = None + endLine: t.Optional[int] = None + endColumn: t.Optional[int] = None + + +class ExceptionBreakpointsFilter(BaseModel): + """An ExceptionBreakpointsFilter is shown in the UI as an option for configuring how exceptions are dealt with.""" + filter: str + label: str + description: t.Optional[str] = None + default: t.Optional[bool] = None + supportsCondition: t.Optional[bool] = None + conditionDescription: t.Optional[str] = None + + +class ExceptionDetails(BaseModel): + """Detailed information about an exception that has occurred.""" + message: t.Optional[str] = None + typeName: t.Optional[str] = None + fullTypeName: t.Optional[str] = None + evaluateName: t.Optional[str] = None + stackTrace: t.Optional[str] = None + innerException: t.Optional[t.List['ExceptionDetails']] = None + + +class ExceptionFilterOptions(BaseModel): + """An ExceptionFilterOptions is used to specify an exception filter together with a condition for the setExceptionBreakpoints request.""" + filterId: str + condition: t.Optional[str] = None + mode: t.Optional[str] = None + + +class ExceptionOptions(BaseModel): + """An ExceptionOptions assigns configuration options to a set of exceptions.""" + path: t.Optional[t.List['ExceptionPathSegment']] = None + breakMode: 'ExceptionBreakMode' + + +class ExceptionBreakMode(BaseModel): + """This enumeration defines all possible conditions when a thrown exception should result in a break.""" + pass + + +class ExceptionPathSegment(BaseModel): + """An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions.""" + negate: t.Optional[bool] = None + names: t.List[str] + + +class FunctionBreakpoint(BaseModel): + """Properties of a breakpoint passed to the setFunctionBreakpoints request.""" + name: str + condition: t.Optional[str] = None + hitCondition: t.Optional[str] = None + + +class GotoTarget(BaseModel): + """A GotoTarget describes a code location that can be used as a target in the 'goto' request.""" + id: int + label: str + line: int + column: t.Optional[int] = None + endLine: t.Optional[int] = None + endColumn: t.Optional[int] = None + instructionPointerReference: t.Optional[str] = None + + +class InstructionBreakpoint(BaseModel): + """Properties of a breakpoint passed to the setInstructionBreakpoints request.""" + instructionReference: str + offset: t.Optional[int] = None + condition: t.Optional[str] = None + hitCondition: t.Optional[str] = None + + +class InvalidatedAreas(BaseModel): + """Logical areas that can be invalidated by the 'invalidated' event.""" + pass + + +class Module(BaseModel): + """A Module object represents a row in the modules view.""" + id: t.Union[int, str] + name: str + path: t.Optional[str] = None + isOptimized: t.Optional[bool] = None + version: t.Optional[str] = None + symbolStatus: t.Optional[str] = None + symbolFilePath: t.Optional[str] = None + dateTimeStamp: t.Optional[str] = None + addressRange: t.Optional[str] = None + + +class Scope(BaseModel): + """A Scope is a named container for variables.""" + name: str + presentationHint: t.Optional[Literal['arguments', 'locals', 'registers']] = None + variablesReference: int + namedVariables: t.Optional[int] = None + indexedVariables: t.Optional[int] = None + expensive: t.Optional[bool] = None + source: t.Optional[Source] = None + line: t.Optional[int] = None + column: t.Optional[int] = None + endLine: t.Optional[int] = None + endColumn: t.Optional[int] = None + + +class SourceBreakpoint(BaseModel): + """Properties of a breakpoint or logpoint passed to the setBreakpoints request.""" + line: int + column: t.Optional[int] = None + condition: t.Optional[str] = None + hitCondition: t.Optional[str] = None + logMessage: t.Optional[str] = None + + +class StackFrame(BaseModel): + """A Stackframe contains the source location.""" + id: int + name: str + source: t.Optional[Source] = None + line: int + column: int + endLine: t.Optional[int] = None + endColumn: t.Optional[int] = None + canRestart: t.Optional[bool] = None + instructionPointerReference: t.Optional[str] = None + moduleId: t.Optional[t.Union[int, str]] = None + presentationHint: t.Optional[Literal['normal', 'label', 'subtle']] = None + + +class StackFrameFormat(BaseModel): + """Provides formatting information for a stack frame.""" + parameters: t.Optional[bool] = None + parameterTypes: t.Optional[bool] = None + parameterNames: t.Optional[bool] = None + parameterValues: t.Optional[bool] = None + line: t.Optional[bool] = None + module: t.Optional[bool] = None + includeAll: t.Optional[bool] = None + + +class StepInTarget(BaseModel): + """A StepInTarget can be used in the 'stepIn' request and identifies an element that can be stepped into.""" + id: int + label: str + + +class SteppingGranularity(BaseModel): + """This enumeration defines all possible stepping granularities.""" + pass + + +class Thread(BaseModel): + """A Thread.""" + id: int + name: str + + +class ValueFormat(BaseModel): + """Provides formatting information for a value.""" + hex: t.Optional[bool] = None + + +class Variable(BaseModel): + """A Variable is a name/value pair.""" + name: str + value: str + type: t.Optional[str] = None + presentationHint: t.Optional['VariablePresentationHint'] = None + evaluateName: t.Optional[str] = None + variablesReference: int + namedVariables: t.Optional[int] = None + indexedVariables: t.Optional[int] = None + memoryReference: t.Optional[str] = None + + +class VariablePresentationHint(BaseModel): + """Optional properties of a variable that can be used to determine how to render the variable in the UI.""" + kind: t.Optional[Literal['property', 'method', 'class', 'data', 'event', 'baseClass', 'innerClass', 'interface', 'mostDerivedClass', 'virtual', 'dataBreakpoint']] = None + attributes: t.Optional[t.List[Literal['static', 'constant', 'readOnly', 'rawString', 'hasObjectId', 'canHaveObjectId', 'hasSideEffects', 'hasDataBreakpoint']]] = None + visibility: t.Optional[Literal['public', 'private', 'protected', 'internal', 'final']] = None + lazy: t.Optional[bool] = None + + +# Update forward references +Source.model_rebuild() +Breakpoint.model_rebuild() +BreakpointMode.model_rebuild() +ExceptionDetails.model_rebuild() +ExceptionOptions.model_rebuild() +Variable.model_rebuild() diff --git a/src/biscuit/common/icons.py b/src/biscuit/common/icons.py index 6cad383e..1b5b5e16 100644 --- a/src/biscuit/common/icons.py +++ b/src/biscuit/common/icons.py @@ -1,4 +1,10 @@ -from enum import StrEnum +try: + from enum import StrEnum +except ImportError: + from enum import Enum + + class StrEnum(str, Enum): + pass class Icons(StrEnum): diff --git a/src/biscuit/common/notifications/manager.py b/src/biscuit/common/notifications/manager.py index d3d283d9..1bcd3db3 100644 --- a/src/biscuit/common/notifications/manager.py +++ b/src/biscuit/common/notifications/manager.py @@ -1,3 +1,4 @@ +import threading import tkinter as tk from typing import Callable @@ -83,6 +84,13 @@ def info( Args: text (str): notification text""" + if threading.current_thread() != threading.main_thread(): + try: + self.after(0, lambda: self.info(text, actions)) + except Exception: + pass + return None + instance = Notification( self, Icons.INFO, text=text, fg=self.base.theme.biscuit, actions=actions ) @@ -104,6 +112,13 @@ def warning( Args: text (str): notification text""" + if threading.current_thread() != threading.main_thread(): + try: + self.after(0, lambda: self.warning(text, actions)) + except Exception: + pass + return None + instance = Notification( self, Icons.WARNING, text=text, fg="yellow", actions=actions ) @@ -125,6 +140,13 @@ def error( Args: text (str): notification text""" + if threading.current_thread() != threading.main_thread(): + try: + self.after(0, lambda: self.error(text, actions)) + except Exception: + pass + return None + instance = Notification(self, Icons.ERROR, text=text, fg="red", actions=actions) instance.pack(side=tk.TOP, fill=tk.BOTH, expand=1, pady=(0, 1)) self.count += 1 diff --git a/src/biscuit/common/palette/item.py b/src/biscuit/common/palette/item.py index 8657974a..7e1bf88d 100644 --- a/src/biscuit/common/palette/item.py +++ b/src/biscuit/common/palette/item.py @@ -33,6 +33,7 @@ def __init__( self.palette = palette self.text = text self.description = description + self.shortcut = kwargs.get('shortcut', '') self.command = command self.theme = self.base.theme @@ -115,3 +116,11 @@ def select(self) -> None: def deselect(self) -> None: self.config(bg=self.bg, fg=self.fg) self.selected = False + + def get_full_display_text(self) -> str: + parts = [self.text] + if getattr(self, "description", ""): + parts.append(f" - {self.description}") + if getattr(self, "shortcut", ""): + parts.append(f" [{self.shortcut}]") + return "".join(parts) diff --git a/src/biscuit/common/ui/native.py b/src/biscuit/common/ui/native.py index 79a8e57d..b13cf147 100644 --- a/src/biscuit/common/ui/native.py +++ b/src/biscuit/common/ui/native.py @@ -57,6 +57,8 @@ class Frame(tk.Frame): """Frame with reference to base""" def __init__(self, master, *args, **kwargs) -> None: + kwargs.pop("foreground", None) + kwargs.pop("fg", None) super().__init__(master, *args, **kwargs) self.master = master self.base: App = master.base diff --git a/src/biscuit/debugger/__init__.py b/src/biscuit/debugger/__init__.py index 3e64194a..d421cae6 100644 --- a/src/biscuit/debugger/__init__.py +++ b/src/biscuit/debugger/__init__.py @@ -1,2 +1,3 @@ from .base import DebuggerBase +from .dap import DAPDebugger, DAPProtocol from .manager import DebuggerManager diff --git a/src/biscuit/debugger/breakpoints.py b/src/biscuit/debugger/breakpoints.py new file mode 100644 index 00000000..fd993ff7 --- /dev/null +++ b/src/biscuit/debugger/breakpoints.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional, Set + + +@dataclass +class Breakpoint: + line: int + condition: Optional[str] = None + hit_condition: Optional[int] = None + hits: int = 0 + + def should_pause(self, scope: Optional[Dict[str, Any]] = None) -> bool: + self.hits += 1 + + if self.hit_condition is not None and self.hits < self.hit_condition: + return False + + if self.condition: + try: + # Safe evaluation in restricted scope + eval_scope = scope if scope is not None else {} + return bool(eval(self.condition, {"__builtins__": {}}, eval_scope)) + except Exception: + return False + + return True + + +class BreakpointManager: + """Manages normal, conditional, and hit-count breakpoints across files.""" + + def __init__(self): + self.breakpoints: Dict[str, Dict[int, Breakpoint]] = {} + + def set_breakpoint( + self, file_path: str, line: int, condition: Optional[str] = None, hit_condition: Optional[int] = None + ) -> Breakpoint: + if file_path not in self.breakpoints: + self.breakpoints[file_path] = {} + + bp = Breakpoint(line=line, condition=condition, hit_condition=hit_condition) + self.breakpoints[file_path][line] = bp + return bp + + def remove_breakpoint(self, file_path: str, line: int) -> Optional[Breakpoint]: + if file_path in self.breakpoints: + return self.breakpoints[file_path].pop(line, None) + return None + + def get_breakpoint(self, file_path: str, line: int) -> Optional[Breakpoint]: + return self.breakpoints.get(file_path, {}).get(line) + + def get_lines(self, file_path: str) -> Set[int]: + return set(self.breakpoints.get(file_path, {}).keys()) + + def should_pause_at(self, file_path: str, line: int, scope: Optional[Dict[str, Any]] = None) -> bool: + bp = self.get_breakpoint(file_path, line) + if not bp: + return False + return bp.should_pause(scope) diff --git a/src/biscuit/debugger/config.py b/src/biscuit/debugger/config.py index 5f2e2fcc..adba60e9 100644 --- a/src/biscuit/debugger/config.py +++ b/src/biscuit/debugger/config.py @@ -5,6 +5,8 @@ import toml +from .watcher import DebuggerConfigWatcher + if typing.TYPE_CHECKING: from .manager import DebuggerManager @@ -18,27 +20,31 @@ def __init__(self, manager: DebuggerManager): self.config_found = False self.configs: list[dict] = [] self.path: Path = None + self.watcher = DebuggerConfigWatcher(self) self.base.bind("<>", self.check, add=True) def check(self, *_) -> None: self.config_found = False + self.configs = [] if not self.base.active_directory: + self.watcher.stop_watch() self.refresh_debugger() return + self.watcher.start_watch() + self.path = Path(self.base.active_directory) / ".biscuit" / CONFIG_FILE if not self.path.exists(): self.refresh_debugger() return self.config_found = True - self.refresh_debugger() - self.load(self.path) def refresh_debugger(self): self.base.debug.refresh() + self.base.debug.refresh_configs() def load(self, path: Path) -> None: with open(path, "r") as f: diff --git a/src/biscuit/debugger/console.py b/src/biscuit/debugger/console.py new file mode 100644 index 00000000..9ee4b227 --- /dev/null +++ b/src/biscuit/debugger/console.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import io +import sys +import traceback +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + + +@dataclass +class ConsoleEvaluationResult: + expression: str + output: str + is_error: bool = False + + +class DebugConsoleSession: + """Manages REPL evaluation and output history for active debug sessions.""" + + def __init__(self): + self.history: List[str] = [] + self.results: List[ConsoleEvaluationResult] = [] + + def evaluate(self, expr: str, scope: Optional[Dict[str, Any]] = None) -> ConsoleEvaluationResult: + expr = expr.strip() + if not expr: + return ConsoleEvaluationResult("", "", False) + + self.history.append(expr) + eval_scope = scope if scope is not None else {} + + stdout_buf = io.StringIO() + old_stdout = sys.stdout + try: + sys.stdout = stdout_buf + try: + # Try eval first for expression results + val = eval(expr, eval_scope) + printed = stdout_buf.getvalue() + res_str = (printed + ("\n" if printed and not printed.endswith("\n") else "") + repr(val)).strip() + result = ConsoleEvaluationResult(expr, res_str, is_error=False) + except SyntaxError: + # Fallback to exec for statements + exec(expr, eval_scope) + res_str = stdout_buf.getvalue().strip() + result = ConsoleEvaluationResult(expr, res_str, is_error=False) + except Exception as e: + err_msg = traceback.format_exc().strip() + result = ConsoleEvaluationResult(expr, err_msg, is_error=True) + finally: + sys.stdout = old_stdout + + self.results.append(result) + return result + + def clear(self) -> None: + self.results.clear() diff --git a/src/biscuit/debugger/dap.py b/src/biscuit/debugger/dap.py new file mode 100644 index 00000000..6139f55c --- /dev/null +++ b/src/biscuit/debugger/dap.py @@ -0,0 +1,534 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +import threading +import typing +from typing import Any + +try: + from dap.client import ClientState, DAPClient + from dap.events import ( + BreakpointEvent, + ContinuedEvent, + ExitedEvent, + InitializedEvent, + LoadedSourceEvent, + ModuleEvent, + OutputEvent, + ProcessEvent, + StoppedEvent, + TerminatedEvent, + ThreadEvent, + ) + from dap.io_handler import _make_event, _make_request, _make_response, _parse_messages + from dap.protocol import ErrorResponse, Event, Request, Response +except ImportError: + from biscuit.common.dap.client import ClientState, DAPClient + from biscuit.common.dap.events import ( + BreakpointEvent, + ContinuedEvent, + ExitedEvent, + InitializedEvent, + LoadedSourceEvent, + ModuleEvent, + OutputEvent, + ProcessEvent, + StoppedEvent, + TerminatedEvent, + ThreadEvent, + ) + from biscuit.common.dap.io_handler import ( + _make_event, + _make_request, + _make_response, + _parse_messages, + ) + from biscuit.common.dap.protocol import ErrorResponse, Event, Request, Response + +from .base import DebuggerBase +from .console import DebugConsoleSession +from .exceptions import ExceptionBreakpointController +from .threads import StackFrame, ThreadCallStackManager, ThreadInfo + +if typing.TYPE_CHECKING: + from .manager import DebuggerManager + + +class DAPProtocol: + """Debug Adapter Protocol parser and serializer backed by debug-adapter-client.""" + + HEADER_CONTENT_LENGTH = "Content-Length: " + HEADER_SEPARATOR = b"\r\n\r\n" + + def __init__(self) -> None: + self.buffer = bytearray() + self.seq = 0 + + def next_seq(self) -> int: + self.seq += 1 + return self.seq + + def feed_data(self, data: bytes | str) -> list[dict[str, Any]]: + """Feed incoming bytes or string and parse complete DAP messages.""" + if isinstance(data, str): + data = data.encode("utf-8") + self.buffer.extend(data) + + messages: list[dict[str, Any]] = [] + try: + for msg in _parse_messages(self.buffer): + if hasattr(msg, "model_dump"): + messages.append(msg.model_dump()) + elif isinstance(msg, dict): + messages.append(msg) + except Exception: + self.buffer.clear() + + return messages + + def build_request( + self, command: str, arguments: dict[str, Any] | None = None + ) -> bytes: + return bytes( + _make_request(command=command, arguments=arguments, seq=self.next_seq()) + ) + + def build_response( + self, + request_seq: int, + command: str, + success: bool = True, + body: dict[str, Any] | None = None, + message: str | None = None, + ) -> bytes: + return bytes( + _make_response( + seq=self.next_seq(), + request_seq=request_seq, + command=command, + success=success, + result=body, + message=message, + ) + ) + + def build_event( + self, event: str, body: dict[str, Any] | None = None + ) -> bytes: + return bytes(_make_event(event=event, body=body, seq=self.next_seq())) + + +class DAPDebugger(DebuggerBase): + """Debug Adapter Protocol (DAP) client implementation subclassing DebuggerBase. + Powered by debug-adapter-client (dap-python). + """ + + def __init__(self, manager: DebuggerManager): + super().__init__(manager) + self.protocol = DAPProtocol() + self.client: DAPClient = self._create_client() + self.process: subprocess.Popen | None = None + self.reader_thread: threading.Thread | None = None + self.is_running = False + self.is_paused = False + self.session_started = False + self.launch_args: dict[str, Any] = {} + self.current_thread_id: int | None = 1 + self.current_frame_id: int | None = None + self.current_variables_ref: int | None = None + self.pending_requests: dict[int, str] = {} # seq -> command name + self.program_path: str = "" + self.cwd: str = "" + self.adapter_command: list[str] = [] + self.exception_controller = ExceptionBreakpointController() + self.threads_manager = ThreadCallStackManager() + self.console = DebugConsoleSession() + + def _create_client(self) -> DAPClient: + return DAPClient( + clientID="biscuit", + clientName="Biscuit Code Editor", + adapterID="dap", + locale="en-US", + linesStartAt1=True, + columnsStartAt1=True, + pathFormat="path", + supportsVariableType=True, + supportsProgressReporting=True, + ) + + def _dispatch_ui(self, func: typing.Callable[[], Any]) -> None: + """Execute a UI callback safely on the main thread.""" + is_mock = hasattr(self.base, "after") and "mock" in type(self.base.after).__module__.lower() + if is_mock: + try: + func() + except Exception: + pass + return + + if ( + threading.current_thread() != threading.main_thread() + and hasattr(self.base, "after") + and callable(self.base.after) + ): + try: + self.base.after(0, func) + return + except Exception: + pass + try: + func() + except Exception: + pass + + def launch_config(self, config: dict[str, Any]) -> None: + self.manager.latest = self + self.launch_args = dict(config) + self.program_path = self.format_path(config.get("program", "")) + self.cwd = self.format_path(config.get("cwd", "")) + + adapter = config.get("adapter") + if isinstance(adapter, list): + self.adapter_command = adapter + elif isinstance(adapter, str): + self.adapter_command = [adapter] + elif not self.adapter_command: + self.adapter_command = [sys.executable, "-m", "debugpy.adapter"] + + self._start_adapter() + + def launch(self, path: str, cwd: str = None) -> None: + self.manager.latest = self + self.program_path = path + self.cwd = cwd or os.path.dirname(os.path.abspath(path)) + self.launch_args = {"program": self.program_path, "cwd": self.cwd} + if not self.adapter_command: + self.adapter_command = [sys.executable, "-m", "debugpy.adapter"] + self._start_adapter() + + def _start_adapter(self) -> None: + self.stop() + if hasattr(self.base, "sidebar"): + self.base.sidebar.show_debug() + if hasattr(self.base, "debug"): + self.base.debug.refresh() + self.base.debug.reset() + self.base.debug.set_running() + + self.is_running = True + self.is_paused = False + self.session_started = False + self.client = self._create_client() + self.pending_requests.clear() + + try: + self.process = subprocess.Popen( + self.adapter_command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=self.cwd or None, + bufsize=0, + ) + # Send initial initialize request buffered by DAPClient + init_bytes = self.client.send() + if init_bytes and self.process.stdin: + self.process.stdin.write(init_bytes) + self.process.stdin.flush() + + self.reader_thread = threading.Thread( + target=self._read_stream, + name="DAP Reader Thread", + daemon=True, + ) + self.reader_thread.start() + except Exception as e: + if hasattr(self.base, "logger"): + self.base.logger.error(f"Failed to start DAP adapter: {e}") + self.stop() + return + + def _read_stream(self) -> None: + while self.is_running and self.process and self.process.stdout: + try: + chunk = self.process.stdout.read(4096) + if not chunk: + break + for event in self.client.recv(chunk): + self.handle_client_event(event) + except Exception: + break + self._on_adapter_exit() + + def handle_client_event(self, event: Any) -> None: + if isinstance(event, Event): + if event.event == "response": + body = event.body or {} + cmd = body.get("command", "") + self.handle_response(cmd, body) + else: + self.handle_event(event.event, event.body or {}) + elif isinstance(event, Response): + self.handle_response(event.command, event.model_dump()) + + def send_request( + self, command: str, arguments: dict[str, Any] | None = None + ) -> int: + arguments = arguments or {} + if ( + self.client.state == ClientState.WAITING_FOR_INITIALIZED + and command != "initialize" + ): + self.client._state = ClientState.NORMAL + + seq = self.client._send_request(command, arguments) + self.pending_requests[seq] = command + + if self.process and self.process.stdin: + try: + data = self.client.send() + if data: + self.process.stdin.write(data) + self.process.stdin.flush() + except Exception: + pass + return seq + + def handle_event(self, event: str, body: dict[str, Any]) -> None: + if event == "initialized": + if self.session_started: + return + self.session_started = True + + # 1. Send launch request + launch_params: dict[str, Any] = { + "program": self.program_path, + "cwd": self.cwd, + "stopOnEntry": self.launch_args.get("stopOnEntry", False), + "console": self.launch_args.get("console", "internalConsole"), + } + for k, v in self.launch_args.items(): + if k not in ("name", "language", "adapter", "program", "cwd"): + launch_params[k] = v + + self.send_request("launch", launch_params) + + # 2. Send initial breakpoints + for path, lines in self.breakpoints.items(): + bps = [] + for item in lines: + if isinstance(item, int): + bps.append({"line": item}) + elif hasattr(item, "line"): + bp_dict: dict[str, Any] = {"line": item.line} + if getattr(item, "condition", None): + bp_dict["condition"] = item.condition + if getattr(item, "hit_condition", None): + bp_dict["hitCondition"] = str(item.hit_condition) + bps.append(bp_dict) + elif isinstance(item, dict): + bps.append(item) + if bps: + self.send_request( + "setBreakpoints", + { + "source": {"path": path}, + "breakpoints": bps, + }, + ) + + # 3. Send exception breakpoints if enabled + if hasattr(self, "exception_controller") and self.exception_controller: + filters = self.exception_controller.get_dap_filters() + if filters: + self.send_request("setExceptionBreakpoints", {"filters": filters}) + + # 4. Configuration done + self.send_request("configurationDone") + + elif event == "stopped": + self.is_paused = True + if "threadId" in body: + self.current_thread_id = body["threadId"] + if hasattr(self.base, "debug"): + self._dispatch_ui(self.base.debug.set_paused) + self.send_request( + "stackTrace", {"threadId": self.current_thread_id or 1} + ) + + elif event == "continued": + self.is_paused = False + if hasattr(self.base, "debug"): + self._dispatch_ui( + lambda: [self.base.debug.reset(), self.base.debug.set_running()] + ) + + elif event == "thread": + reason = body.get("reason") + thread_id = body.get("threadId") + if reason == "started" and thread_id is not None: + t_info = ThreadInfo(id=thread_id, name=f"Thread-{thread_id}", status="running") + self.threads_manager.update_threads(list(self.threads_manager.threads.values()) + [t_info]) + elif reason == "exited" and thread_id is not None: + self.threads_manager.threads.pop(thread_id, None) + + elif event in ("terminated", "exited"): + self.stop() + + elif event == "output": + output_text = body.get("output", "") + category = body.get("category", "output") + if hasattr(self.base, "logger"): + self.base.logger.info(f"[DAP {category}] {output_text.strip()}") + + def handle_response( + self, command: str, msg: dict[str, Any] + ) -> None: + if not msg.get("success", False): + return + + body = msg.get("body", {}) or {} + if command == "stackTrace": + frames = body.get("stackFrames", []) + stack = [] + stack_objs = [] + for f in frames: + name = f.get("name", "") + source = f.get("source", {}) + path = source.get("path", "") if isinstance(source, dict) else "" + line = f.get("line", 1) + frame_id = f.get("id", 0) + stack.append((name, path, line)) + stack_objs.append(StackFrame(id=frame_id, name=name, file=path, line=line)) + if hasattr(self, "callstack") and self.callstack: + self._dispatch_ui(lambda: self.callstack.show(stack)) + + if self.current_thread_id is not None: + t_info = ThreadInfo( + id=self.current_thread_id, + name=f"Thread-{self.current_thread_id}", + status="paused", + frames=stack_objs, + ) + self.threads_manager.update_threads([t_info]) + + if frames: + self.current_frame_id = frames[0].get("id") + self.send_request("scopes", {"frameId": self.current_frame_id}) + + elif command == "scopes": + scopes = body.get("scopes", []) + for scope in scopes: + ref = scope.get("variablesReference") + if ref: + self.current_variables_ref = ref + self.send_request( + "variables", + {"variablesReference": ref, "scopeName": scope.get("name", "Locals")}, + ) + + elif command == "variables": + var_list = body.get("variables", []) + vars_dict = { + var.get("name", ""): var.get("value", "") for var in var_list + } + scope_name = msg.get("scopeName") or "Locals" + if hasattr(self, "variables") and self.variables: + self._dispatch_ui(lambda: self.variables.show(scope_name, vars_dict, open_=True)) + + def _on_adapter_exit(self) -> None: + self.is_running = False + self.is_paused = False + self.session_started = False + if hasattr(self.base, "debug"): + self._dispatch_ui( + lambda: [ + self.base.debug.reset(), + self.base.debug.set_stopped(), + self.base.debug.refresh(), + ] + ) + + def update_breakpoints( + self, file_path: str, line_numbers: set[int] + ) -> None: + super().update_breakpoints(file_path, line_numbers) + if self.is_running: + self.send_request( + "setBreakpoints", + { + "source": {"path": file_path}, + "breakpoints": [{"line": line} for line in line_numbers], + }, + ) + + def stop(self) -> None: + if self.is_running: + try: + self.send_request("disconnect", {"terminateDebuggee": True}) + except Exception: + pass + + if self.process: + try: + self.process.terminate() + self.process.wait(timeout=1) + except Exception: + try: + self.process.kill() + except Exception: + pass + self.process = None + + self._on_adapter_exit() + + def restart(self) -> None: + self.stop() + if self.program_path: + self.launch(self.program_path, self.cwd) + + def step_in(self) -> None: + if self.is_paused: + self.send_request("stepIn", {"threadId": self.current_thread_id or 1}) + + def step_over(self) -> None: + if self.is_paused: + self.send_request("next", {"threadId": self.current_thread_id or 1}) + + def step_out(self) -> None: + if self.is_paused: + self.send_request("stepOut", {"threadId": self.current_thread_id or 1}) + + def continue_pause(self) -> None: + if self.is_paused: + self.send_request( + "continue", {"threadId": self.current_thread_id or 1} + ) + self.is_paused = False + if hasattr(self.base, "debug"): + self.base.debug.reset() + self.base.debug.set_running() + else: + self.send_request("pause", {"threadId": self.current_thread_id or 1}) + + def set_variable(self, name: str, value: str) -> None: + if self.current_variables_ref is not None: + self.send_request( + "setVariable", + { + "variablesReference": self.current_variables_ref, + "name": name, + "value": value, + }, + ) + + def evaluate(self, expression: str, frame_id: int | None = None) -> None: + frame = frame_id if frame_id is not None else self.current_frame_id + args: dict[str, Any] = {"expression": expression} + if frame is not None: + args["frameId"] = frame + self.send_request("evaluate", args) diff --git a/src/biscuit/debugger/exceptions.py b/src/biscuit/debugger/exceptions.py new file mode 100644 index 00000000..63bb6b6e --- /dev/null +++ b/src/biscuit/debugger/exceptions.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from enum import Enum +from typing import List + + +class ExceptionBreakMode(str, Enum): + NEVER = "never" + UNCAUGHT = "uncaught" + ALL = "all" + + +class ExceptionBreakpointController: + """Manages exception breakpoint filters for debug sessions.""" + + def __init__(self, break_on_uncaught: bool = True, break_on_all: bool = False): + self.break_on_uncaught = break_on_uncaught + self.break_on_all = break_on_all + + def set_break_on_uncaught(self, enabled: bool) -> None: + self.break_on_uncaught = enabled + + def set_break_on_all(self, enabled: bool) -> None: + self.break_on_all = enabled + + def should_break_on_exception(self, is_uncaught: bool) -> bool: + if self.break_on_all: + return True + if self.break_on_uncaught and is_uncaught: + return True + return False + + def get_dap_filters(self) -> List[str]: + filters = [] + if self.break_on_uncaught: + filters.append("uncaught") + if self.break_on_all: + filters.append("userUnhandled") + filters.append("all") + return filters diff --git a/src/biscuit/debugger/manager.py b/src/biscuit/debugger/manager.py index 7f3d58b4..bf4387b0 100644 --- a/src/biscuit/debugger/manager.py +++ b/src/biscuit/debugger/manager.py @@ -7,6 +7,7 @@ from .base import DebuggerBase from .config import ConfigLoader +from .dap import DAPDebugger from .python import PythonDebugger if typing.TYPE_CHECKING: @@ -25,6 +26,7 @@ def __init__(self, base: App): {} ) # language -> debugger type mapping self.debuggers[Languages.PYTHON] = PythonDebugger + self.debuggers["dap"] = DAPDebugger self.spawned: dict[str, DebuggerBase] = ( {} @@ -114,11 +116,28 @@ def register_debugger(self, language: str, debugger: type[DebuggerBase]) -> None language (str): the language debugger (type[DebuggerBase]): the debugger type """ - self.debuggers[language] = debugger + self.debuggers[language.lower()] = debugger + + def register_dap_adapter( + self, language: str, adapter_command: list[str] | str + ) -> None: + """Register a DAP-based debugger for a language using the given adapter command. + Args: + language (str): the language (e.g. 'rust', 'go', 'python') + adapter_command (list[str] | str): command to start the DAP adapter executable + """ + cmd = [adapter_command] if isinstance(adapter_command, str) else list(adapter_command) + + class CustomDAPDebugger(DAPDebugger): + def __init__(self, manager: DebuggerManager): + super().__init__(manager) + self.adapter_command = list(cmd) + + self.register_debugger(language.lower(), CustomDAPDebugger) def unregister_debugger(self, language: str) -> None: """Unregister a debugger for a specific language. Args: language (str): the language """ - self.debuggers.pop(language, None) + self.debuggers.pop(language.lower(), None) diff --git a/src/biscuit/debugger/python.py b/src/biscuit/debugger/python.py index be8b5e0b..c4898d06 100644 --- a/src/biscuit/debugger/python.py +++ b/src/biscuit/debugger/python.py @@ -1,171 +1,18 @@ from __future__ import annotations -import ast -import bdb import os import sys -import threading import typing -from types import CodeType -from .base import DebuggerBase -from .pyutils import * +from .dap import DAPDebugger if typing.TYPE_CHECKING: from .manager import DebuggerManager -class PythonDebugger(DebuggerBase, bdb.Bdb): - """Python debugger implementation using the built-in `bdb` module. - Uses biscuit's debugger base class to interact with the app.""" +class PythonDebugger(DAPDebugger): + """Python debugger implementation using Debug Adapter Protocol (debugpy).""" def __init__(self, manager: DebuggerManager): - DebuggerBase.__init__(self, manager) - bdb.Bdb.__init__(self) - self.current_frame = None - self.is_running = False - self.debug = self.base.debug - - # Event to pause the debugger - self.debugging = threading.Event() - - self.latest_path = "" - - def launch(self, path: str, cwd="") -> None: - self.reset() - self.base.sidebar.show_debug() - self.debug.refresh() - self.latest_path = path - - script_dir = os.path.dirname(os.path.abspath(path)) - if script_dir not in sys.path: - sys.path.insert(0, script_dir) - - with open(path, "r") as f: - code = compile(f.read(), path, "exec") - - self.clear_all_breaks() - for path, lines in self.breakpoints.items(): - for line in lines: - self.set_break(path.replace("\\", "/"), line) - - globals_dict = { - "__name__": "__main__", - "__file__": path, - "__builtins__": __builtins__, - } - - self.is_running = True - self.thread = threading.Thread( - target=self._run_debugger, - name="Python Debugger Thread", - args=(code, globals_dict, script_dir, cwd), - daemon=True, - ) - self.thread.start() - - def _run_debugger( - self, - code: CodeType, - globals_dict: dict[str, typing.Any], - script_dir: str, - cwd="", - ): - original_cwd = cwd or os.getcwd() - try: - os.chdir(script_dir) - self.set_trace() - - # Update UI - self.debug.reset() - self.debug.set_running() - - exec(code, globals_dict) - except bdb.BdbQuit: - pass - except Exception as e: - print(f"Debugger internal error: {e}") - finally: - os.chdir(original_cwd) - self.is_running = False - - # Update UI - self.debug.reset() - self.debug.set_stopped() - self.debug.refresh() - - def user_line(self, frame): - self.debug.reset() - self.current_frame = frame - if self.break_here(frame): - self.debugging.clear() - - for d in get_variables(frame): - self.variables.show(*d) - self.callstack.show(get_callstack(frame)) - - self.debug.set_paused() # Update UI to show we're paused - self.debugging.wait() # Wait for user input - - def user_return(self, frame, return_value): ... - - def user_exception(self, frame, exc_info): - self.debugging.clear() - self.current_frame = frame - self.variables.show(frame.f_locals.items()) - self.callstack.show(get_callstack(frame)) - self.debug.set_paused() - self.debugging.wait() - - def stop(self): - self.is_running = False - self.set_quit() - self.debugging.set() # Release any waiting threads - - def restart(self): - self.stop() - self.launch(self.latest_path) - - def step_in(self): - self.set_step() - self.debugging.set() - - def step_over(self): - self.set_next(self.current_frame) - self.debugging.set() - - def step_out(self): - self.set_return(self.current_frame) - self.debugging.set() - - def continue_pause(self): - self.set_continue() - - if self.debugging.is_set(): - self.debugging.clear() - else: - self.debugging.set() - - def set_variable(self, name: str, value: str): - try: - # evaluate as a literal - new_value = ast.literal_eval(value) - print(f"Value of `{name}` changed to {new_value} (literal)") - except (ValueError, SyntaxError): - try: - # evaluate expression in current frame - new_value = eval( - value, self.current_frame.f_globals, self.current_frame.f_locals - ) - print(f"Value of `{name}` changed to {new_value} (complex expression)") - except Exception as e: - print(f"Value of `{name}` cannot be changed: {e}") - return - - self.current_frame.f_locals[name] = new_value - save_locals(self.current_frame) - - # Update UI - self.variables.clear() - for d in get_variables(self.current_frame): - self.variables.show(*d) + super().__init__(manager) + self.adapter_command = [sys.executable, "-m", "debugpy.adapter"] diff --git a/src/biscuit/debugger/threads.py b/src/biscuit/debugger/threads.py new file mode 100644 index 00000000..7bda7966 --- /dev/null +++ b/src/biscuit/debugger/threads.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List, Optional + + +@dataclass +class StackFrame: + id: int + name: str + file: str + line: int + + +@dataclass +class ThreadInfo: + id: int + name: str + status: str = "paused" + frames: List[StackFrame] = field(default_factory=list) + + +class ThreadCallStackManager: + """Manages multi-threaded process call stack inspection in debugger.""" + + def __init__(self): + self.threads: Dict[int, ThreadInfo] = {} + self.active_thread_id: Optional[int] = None + + def update_threads(self, thread_list: List[ThreadInfo]) -> None: + self.threads = {t.id: t for t in thread_list} + if self.threads: + if self.active_thread_id not in self.threads: + self.active_thread_id = next(iter(self.threads.keys())) + else: + self.active_thread_id = None + + def select_thread(self, thread_id: int) -> Optional[ThreadInfo]: + if thread_id in self.threads: + self.active_thread_id = thread_id + return self.threads[thread_id] + return None + + def get_active_thread(self) -> Optional[ThreadInfo]: + if self.active_thread_id is not None: + return self.threads.get(self.active_thread_id) + return None + + def get_active_frames(self) -> List[StackFrame]: + thread = self.get_active_thread() + return thread.frames if thread else [] diff --git a/src/biscuit/debugger/watcher.py b/src/biscuit/debugger/watcher.py new file mode 100644 index 00000000..46a62f98 --- /dev/null +++ b/src/biscuit/debugger/watcher.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import os +import typing +from pathlib import Path + +from watchdog.events import PatternMatchingEventHandler +from watchdog.observers import Observer + +if typing.TYPE_CHECKING: + from .config import ConfigLoader + + +class DebuggerConfigWatcher(PatternMatchingEventHandler): + """Watches the workspace `.biscuit` directory for changes to `launch.toml`.""" + + def __init__(self, config_loader: ConfigLoader) -> None: + super().__init__(patterns=["*launch.toml"], ignore_directories=True) + self.config_loader = config_loader + self.base = config_loader.base + self.observer: typing.Optional[Observer] = None + self._scheduled = False + + def start_watch(self) -> None: + self.stop_watch() + + if not self.base.active_directory: + return + + watch_dir = Path(self.base.active_directory) / ".biscuit" + if not watch_dir.exists(): + return + + try: + self.observer = Observer() + self.observer.schedule(self, str(watch_dir), recursive=False) + self.observer.start() + self._scheduled = True + except Exception as e: + if hasattr(self.base, "logger") and self.base.logger: + self.base.logger.warning(f"DebuggerConfigWatcher failed to start: {e}") + + def stop_watch(self) -> None: + if self.observer: + try: + self.observer.stop() + self.observer.join(timeout=1) + except Exception: + pass + self.observer = None + self._scheduled = False + + def on_created(self, event) -> None: + self.config_loader.check() + + def on_modified(self, event) -> None: + self.config_loader.check() + + def on_deleted(self, event) -> None: + self.config_loader.check() diff --git a/src/biscuit/editor/autocomplete/item.py b/src/biscuit/editor/autocomplete/item.py index 5a74685c..0cc99ed2 100644 --- a/src/biscuit/editor/autocomplete/item.py +++ b/src/biscuit/editor/autocomplete/item.py @@ -29,7 +29,7 @@ def __init__(self, master: AutoComplete, *args, **kwargs) -> None: self.replace_end: str = "" self.replace_text: str = "" self.documentation: str = "" - # TODO add GUI for documentation + self.doc_popup_enabled = True self.kind = Kind(self) self.text = tk.Text( @@ -149,3 +149,6 @@ def deselect(self): self.kind.config(bg=self.bg) self.text.config(bg=self.bg, fg=self.fg) self.selected = False + + def get_documentation_text(self) -> str: + return getattr(self, "documentation", "") or getattr(self, "detail", "") diff --git a/src/biscuit/editor/breadcrumbs/breadcrumbs.py b/src/biscuit/editor/breadcrumbs/breadcrumbs.py index 6975882b..c5c2b993 100644 --- a/src/biscuit/editor/breadcrumbs/breadcrumbs.py +++ b/src/biscuit/editor/breadcrumbs/breadcrumbs.py @@ -34,6 +34,23 @@ def on_click(self, e: tk.Event): # TODO show current symbols when language features are enabled class BreadCrumbs(Frame): + def update_symbols(self, symbols: list) -> None: + self.symbols = symbols + # Clear existing symbol buttons + if hasattr(self, "symbol_buttons"): + for b in self.symbol_buttons: + b.destroy() + self.symbol_buttons = [] + for sym in symbols: + name = sym.get("name", "") if isinstance(sym, dict) else getattr(sym, "name", str(sym)) + btn = Item(self.container, name, callback=lambda _, s=sym: self.on_symbol_click(s)) + btn.pack(side=tk.LEFT) + self.symbol_buttons.append(btn) + + def on_symbol_click(self, symbol) -> None: + if hasattr(self.master, "on_symbol_click"): + self.master.on_symbol_click(symbol) + """Breadcrumbs widget Breadcrumbs widget is used to display the path of the current file. diff --git a/src/biscuit/editor/breadcrumbs/history_navigation.py b/src/biscuit/editor/breadcrumbs/history_navigation.py index ed13ab84..053eba55 100644 --- a/src/biscuit/editor/breadcrumbs/history_navigation.py +++ b/src/biscuit/editor/breadcrumbs/history_navigation.py @@ -49,6 +49,51 @@ def __init__(self, master: BreadCrumbs, *args, **kwargs) -> None: self.left.pack(side=tk.LEFT, fill=tk.BOTH) self.right.pack(side=tk.RIGHT, fill=tk.BOTH) - # TODO: Implement go_up and go_down methods - def go_up(self) -> None: ... - def go_down(self) -> None: ... + def update_buttons(self) -> None: + editor = getattr(self.base.editorsmanager, "active_editor", None) + if not editor or not hasattr(editor, "content"): + self.left.config(state=tk.DISABLED) + self.right.config(state=tk.DISABLED) + return + + history = getattr(editor.content, "cursor_history", []) + index = getattr(editor.content, "cursor_history_index", -1) + + self.left.config(state=tk.NORMAL if index > 0 else tk.DISABLED) + self.right.config(state=tk.NORMAL if index < len(history) - 1 else tk.DISABLED) + + def go_down(self, *_) -> None: + """Navigate backward in cursor history.""" + editor = getattr(self.base.editorsmanager, "active_editor", None) + if not editor or not hasattr(editor, "content"): + return + + content = editor.content + history = getattr(content, "cursor_history", []) + index = getattr(content, "cursor_history_index", -1) + + if index > 0 and history: + content.cursor_history_index = index - 1 + pos = history[content.cursor_history_index] + if hasattr(content, "text"): + content.text.mark_set(tk.INSERT, pos) + content.text.see(pos) + self.update_buttons() + + def go_up(self, *_) -> None: + """Navigate forward in cursor history.""" + editor = getattr(self.base.editorsmanager, "active_editor", None) + if not editor or not hasattr(editor, "content"): + return + + content = editor.content + history = getattr(content, "cursor_history", []) + index = getattr(content, "cursor_history_index", -1) + + if index < len(history) - 1: + content.cursor_history_index = index + 1 + pos = history[content.cursor_history_index] + if hasattr(content, "text"): + content.text.mark_set(tk.INSERT, pos) + content.text.see(pos) + self.update_buttons() diff --git a/src/biscuit/editor/findreplace/find_replace.py b/src/biscuit/editor/findreplace/find_replace.py index 705461f4..ee6ae2e4 100644 --- a/src/biscuit/editor/findreplace/find_replace.py +++ b/src/biscuit/editor/findreplace/find_replace.py @@ -50,9 +50,8 @@ def __init__(self, base, *args, **kwargs) -> None: buttons.grid(row=0, column=2, sticky=tk.NSEW, pady=2) IconButton(buttons, Icons.ARROW_UP, self.prev_match).pack(side=tk.LEFT) IconButton(buttons, Icons.ARROW_DOWN, self.next_match).pack(side=tk.LEFT) - IconButton(buttons, Icons.LIST_SELECTION).pack( - side=tk.LEFT - ) # TODO add finding from selection + self.find_in_sel_btn = IconButton(buttons, Icons.LIST_SELECTION, self.toggle_find_in_selection) + self.find_in_sel_btn.pack(side=tk.LEFT) IconButton(buttons, Icons.CLOSE, self.hide).pack(side=tk.LEFT) # replace @@ -236,3 +235,9 @@ def replace_all(self, *_): self.next_match() self.replace() self.text.mark_set("insert", f"1.0 + {current}c") + + def toggle_find_in_selection(self, *_): + self.find_in_selection = not getattr(self, "find_in_selection", False) + if hasattr(self, "find_in_sel_btn"): + self.find_in_sel_btn.set_color(self.base.theme.biscuit if self.find_in_selection else None) + return self.find_in_selection diff --git a/src/biscuit/editor/hover/hover.py b/src/biscuit/editor/hover/hover.py index d7f1b5f3..077e9d1b 100644 --- a/src/biscuit/editor/hover/hover.py +++ b/src/biscuit/editor/hover/hover.py @@ -35,7 +35,17 @@ def __init__(self, master: App, bd: int = 2, *args, **kw) -> None: self.withdraw() - def update_position(self, pos: str, tab: Text): + def calculate_size(self, docs: str) -> tuple[int, int]: + """Calculate responsive width and height based on documentation text length and lines.""" + lines = docs.strip().split("\n") + line_count = len(lines) + max_line_len = max((len(l) for l in lines), default=20) + + width = min(max(max_line_len * 8 + 30, 220), 550) + height = min(max(line_count * 22 + 40, 50), 220) + return width, height + + def update_position(self, pos: str, tab: Text, width: int | None = None, height: int | None = None): tab.update_idletasks() pos_x, pos_y = tab.winfo_rootx(), tab.winfo_rooty() @@ -46,9 +56,16 @@ def update_position(self, pos: str, tab: Text): return bbx_x, bbx_y, _, _ = bbox - self.geometry( - "+{}+{}".format(pos_x + bbx_x - 1, pos_y + bbx_y - self.winfo_height()) - ) + h = height if height else self.winfo_height() + w = width if width else self.winfo_width() + top_y = pos_y + bbx_y - h + if top_y < 0: + top_y = pos_y + bbx_y + 20 + + if width and height: + self.geometry(f"{w}x{h}+{pos_x + bbx_x - 1}+{top_y}") + else: + self.geometry(f"+{pos_x + bbx_x - 1}+{top_y}") def show(self, tab: Text, response: HoverResponse) -> None: if not response or not (response.text or response.docs): @@ -69,11 +86,11 @@ def show(self, tab: Text, response: HoverResponse) -> None: self.hide() return + w, h = self.calculate_size(docs) self.renderer.render_markdown(docs) self.update_idletasks() self.deiconify() - self.update() - self.update_position(response.location, tab) + self.update_position(response.location, tab, width=w, height=h) def hide(self, *_) -> None: self.withdraw() diff --git a/src/biscuit/editor/html/editor.py b/src/biscuit/editor/html/editor.py index de82c28a..f16b676f 100644 --- a/src/biscuit/editor/html/editor.py +++ b/src/biscuit/editor/html/editor.py @@ -21,7 +21,7 @@ def __init__(self, master, path, exists=False, *args, **kwargs) -> None: # TODO open in browser button - self.__buttons__ = ((Icons.OPEN_PREVIEW, self.toggle_preview),) + self.__buttons__ = ((Icons.OPEN_PREVIEW, self.toggle_preview), (Icons.GLOBE, self.open_in_browser)) self.editor = TextEditor(self, path, exists=exists) self.editor.grid(row=0, column=0, sticky=tk.NSEW, padx=(0, 1)) @@ -67,3 +67,8 @@ def on_textscroll(self, *args) -> None: def on_change(self, *_): if self.preview_enabled: self.renderer.refresh() + + def open_in_browser(self, *_) -> None: + import webbrowser + if self.path: + webbrowser.open_new_tab(f"file://{self.path}") diff --git a/src/biscuit/editor/text/column_selection.py b/src/biscuit/editor/text/column_selection.py new file mode 100644 index 00000000..db08cad1 --- /dev/null +++ b/src/biscuit/editor/text/column_selection.py @@ -0,0 +1,18 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import List, Tuple + +@dataclass +class ColumnSelection: + start_row: int + start_col: int + end_row: int + end_col: int + + def get_line_ranges(self) -> List[Tuple[str, str]]: + min_row, max_row = sorted([self.start_row, self.end_row]) + min_col, max_col = sorted([self.start_col, self.end_col]) + return [ + (f"{r}.{min_col}", f"{r}.{max_col}") + for r in range(min_row, max_row + 1) + ] diff --git a/src/biscuit/editor/text/editor.py b/src/biscuit/editor/text/editor.py index b6734f59..cb6ac0f8 100644 --- a/src/biscuit/editor/text/editor.py +++ b/src/biscuit/editor/text/editor.py @@ -46,6 +46,8 @@ def __init__( self.runmenu = None self.unsupported = False self.content_hash = "" + self.cursor_history: list[str] = [] + self.cursor_history_index: int = -1 if not self.standalone: self.__buttons__ = [ @@ -240,8 +242,24 @@ def on_change(self, *_) -> None: self.text.refresh() if not self.minimalist: self.minimap.redraw_cursor() + self.record_cursor_position() self.event_generate("<>") + def record_cursor_position(self, pos: str = None) -> None: + try: + if not pos and hasattr(self, "text"): + pos = self.text.index(tk.INSERT) + if not pos: + return + if not self.cursor_history or self.cursor_history[-1] != pos: + self.cursor_history.append(pos) + self.cursor_history_index = len(self.cursor_history) - 1 + if len(self.cursor_history) > 100: + self.cursor_history.pop(0) + self.cursor_history_index -= 1 + except Exception: + pass + def on_scroll(self, *_) -> None: self.linenumbers.redraw() if not self.minimalist: diff --git a/src/biscuit/editor/text/folding.py b/src/biscuit/editor/text/folding.py new file mode 100644 index 00000000..2e043fc1 --- /dev/null +++ b/src/biscuit/editor/text/folding.py @@ -0,0 +1,40 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Dict, List, Tuple + +@dataclass +class FoldRange: + start_line: int + end_line: int + is_folded: bool = False + +class CodeFoldingManager: + def __init__(self): + self.folds: Dict[int, FoldRange] = {} + + def add_fold(self, start_line: int, end_line: int) -> FoldRange: + fold = FoldRange(start_line, end_line) + self.folds[start_line] = fold + return fold + + def toggle_fold(self, start_line: int) -> bool: + if start_line in self.folds: + self.folds[start_line].is_folded = not self.folds[start_line].is_folded + return self.folds[start_line].is_folded + return False + + def is_line_folded(self, start_line: int) -> bool: + return self.folds.get(start_line, FoldRange(0, 0)).is_folded + + def compute_indent_folds(self, lines: List[str]) -> List[Tuple[int, int]]: + folds = [] + for i, line in enumerate(lines): + indent = len(line) - len(line.lstrip()) + if line.rstrip().endswith(":") or line.rstrip().endswith("{"): + # find end + for j in range(i + 1, len(lines)): + next_line = lines[j] + if next_line.strip() and (len(next_line) - len(next_line.lstrip())) <= indent: + folds.append((i + 1, j)) + break + return folds diff --git a/src/biscuit/editor/text/highlighter.py b/src/biscuit/editor/text/highlighter.py index d59804f6..564da0e2 100644 --- a/src/biscuit/editor/text/highlighter.py +++ b/src/biscuit/editor/text/highlighter.py @@ -57,6 +57,10 @@ def incremental_highlight(self, edit_info: dict) -> None: """Incremental highlight after an edit.""" self.ts.incremental_highlight(edit_info) + def get_matching_bracket(self, line: int, col: int) -> tuple[int, int] | None: + """Get matching bracket coordinates from tree-sitter highlighter.""" + return self.ts.get_matching_bracket(line, col) + # ============================================================================= # ORIGINAL PYGMENTS IMPLEMENTATION (disabled — kept for reference) diff --git a/src/biscuit/editor/text/linenumbers.py b/src/biscuit/editor/text/linenumbers.py index 0ffbf67a..d9ff36e8 100644 --- a/src/biscuit/editor/text/linenumbers.py +++ b/src/biscuit/editor/text/linenumbers.py @@ -70,6 +70,9 @@ def set_bar_width(self, width): self.configure(width=width) def toggle_breakpoint(self, line): + if not getattr(self.master, "debugger", None): + return + if line in self.breakpoints: self.breakpoints.remove(line) else: diff --git a/src/biscuit/editor/text/minimap.py b/src/biscuit/editor/text/minimap.py index f7468272..eaf93db5 100644 --- a/src/biscuit/editor/text/minimap.py +++ b/src/biscuit/editor/text/minimap.py @@ -13,6 +13,16 @@ # TODO update minimap when scrollbar is used # TODO rewrite the handling with proper yview (percentage change) class Minimap(Frame): + def update_from_yview(self, first: float, last: float) -> None: + try: + height = self.cw.winfo_height() + if height > 1: + target_y = int(float(first) * height) + self.cw.coords("slider", 0, target_y) + self.yvalue = target_y + except Exception: + pass + def __init__(self, master: TextEditor, text: Text = None, *args, **kwargs) -> None: super().__init__(master, *args, **kwargs) self.tw = text diff --git a/src/biscuit/editor/text/sticky_scroll.py b/src/biscuit/editor/text/sticky_scroll.py new file mode 100644 index 00000000..2fdce734 --- /dev/null +++ b/src/biscuit/editor/text/sticky_scroll.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import List, Optional + +@dataclass +class StickyHeaderScope: + line_number: int + header_text: str + +class StickyScrollController: + def __init__(self): + self.scopes: List[StickyHeaderScope] = [] + + def set_scopes(self, scopes: List[StickyHeaderScope]): + self.scopes = sorted(scopes, key=lambda s: s.line_number) + + def get_active_headers(self, current_top_line: int) -> List[str]: + active = [] + for s in self.scopes: + if s.line_number < current_top_line: + active.append(s.header_text) + else: + break + return active[-2:] # Show up to 2 enclosing scopes diff --git a/src/biscuit/editor/text/text.py b/src/biscuit/editor/text/text.py index d5ead892..6a4ea1c5 100644 --- a/src/biscuit/editor/text/text.py +++ b/src/biscuit/editor/text/text.py @@ -103,6 +103,7 @@ def __init__( # self.last_change = Change(None, None, None, None, None) self._pending_edit_info = None + self.config_tags() self.highlighter = Highlighter(self, language) if not self.standalone and not self.minimalist: self.base.statusbar.on_open_file(self) @@ -113,7 +114,6 @@ def __init__( self.diagnostics = MinClosestKeyDict() self.focus_set() - self.config_tags() self.create_proxy() self.config_bindings() self.update_idletasks() @@ -486,13 +486,30 @@ def get_active_block_info(self) -> None: self.active_start_line = 1 self.active_end_line = int(self.index(tk.END).split(".")[0]) - # TODO this wont work properly - # write a custom ast, parser for bracket matching def highlight_current_brackets(self): self.tag_remove("activebracket", "1.0", tk.END) i = self.index(tk.INSERT) - start, end = None, None + # 1. Try AST-based bracket matching via tree-sitter highlighter + if hasattr(self, "highlighter") and hasattr(self.highlighter, "get_matching_bracket"): + try: + parts = i.split(".") + line, col = int(parts[0]), int(parts[1]) + match = self.highlighter.get_matching_bracket(line, col) + current_pos = f"{line}.{col}" + if not match and col > 0: + match = self.highlighter.get_matching_bracket(line, col - 1) + current_pos = f"{line}.{col - 1}" + if match and current_pos: + match_pos = f"{match[0]}.{match[1]}" + self.tag_add("activebracket", current_pos, current_pos + "+1c") + self.tag_add("activebracket", match_pos, match_pos + "+1c") + return + except Exception: + pass + + # 2. Fallback to basic regexp search + start, end = None, None try: if start := self.search( r"[\{\[\(\)\]\}]", i, i + " linestart", backwards=True, regexp=True @@ -1105,9 +1122,44 @@ def check_indentation(self, *args): return "break" def multi_selection(self, *args): - # TODO: multi cursor editing + if not hasattr(self, "cursors"): + self.cursors = [] + # Add current insert position as cursor + pos = self.index(tk.INSERT) + if pos not in self.cursors: + self.cursors.append(pos) return "break" + def add_cursor(self, index: str): + if not hasattr(self, "cursors"): + self.cursors = [] + if index not in self.cursors: + self.cursors.append(index) + + def clear_cursors(self): + self.cursors = [] + + def request_hide_hover(self, delay: int = 300) -> None: + if hasattr(self, "_hover_hide_timer") and self._hover_hide_timer: + self.after_cancel(self._hover_hide_timer) + self._hover_hide_timer = self.after(delay, self._dismiss_hover_if_outside) + + def cancel_hide_hover(self) -> None: + if hasattr(self, "_hover_hide_timer") and self._hover_hide_timer: + self.after_cancel(self._hover_hide_timer) + self._hover_hide_timer = None + + def _dismiss_hover_if_outside(self) -> None: + if hasattr(self, "hover") and self.hover: + self.hover.hide() + self._hover_hide_timer = None + + def toggle_word_wrap(self) -> str: + current = self.cget("wrap") + new_wrap = "none" if current == "word" else "word" + self.configure(wrap=new_wrap) + return new_wrap + def open_find_replace(self, *_): self.base.findreplace.show(self) diff --git a/src/biscuit/editor/text/ts_highlighter.py b/src/biscuit/editor/text/ts_highlighter.py index b4b0b916..20038db9 100644 --- a/src/biscuit/editor/text/ts_highlighter.py +++ b/src/biscuit/editor/text/ts_highlighter.py @@ -237,6 +237,10 @@ def __init__(self, text: Text, language: str = None, *args, **kwargs) -> None: self.query: Query | None = None self.language_name: str | None = None self.tag_colors: dict = self.base.theme.treesitter_syntax + self.bracket_colors = getattr( + self.base.theme.editors, "bracket_colors", ("ffd700", "da70d6", "179fff") + ) + self.bracket_map: dict[tuple[int, int], tuple[int, int]] = {} if not TREE_SITTER_AVAILABLE: return @@ -331,10 +335,24 @@ def setup_highlight_tags(self) -> None: else: self.text.tag_configure(tag, foreground=props) + self.bracket_colors = getattr( + self.base.theme.editors, "bracket_colors", ("ffd700", "da70d6", "179fff") + ) + for i, color in enumerate(self.bracket_colors): + c = f"#{color}" if not str(color).startswith("#") else color + self.text.tag_configure(f"bracket.depth{i}", foreground=c) + try: + self.text.tag_raise("activebracket") + except Exception: + pass + def clear(self) -> None: """Remove all Tree-sitter highlight tags.""" for capture_name in self.tag_colors: self.text.tag_remove(f"ts.{capture_name}", "1.0", tk.END) + for i in range(len(self.bracket_colors)): + self.text.tag_remove(f"bracket.depth{i}", "1.0", tk.END) + self.bracket_map.clear() def highlight(self) -> None: """Full parse and highlight (used on file load / language change).""" @@ -381,6 +399,9 @@ def _apply_highlights_full(self) -> None: # Clear all existing tree-sitter tags for capture_name in self.tag_colors: self.text.tag_remove(f"ts.{capture_name}", "1.0", tk.END) + for i in range(len(self.bracket_colors)): + self.text.tag_remove(f"bracket.depth{i}", "1.0", tk.END) + self.bracket_map.clear() if not self.tree or not self.query: return @@ -397,6 +418,8 @@ def _apply_highlights_full(self) -> None: end = f"{node.end_point[0] + 1}.{node.end_point[1]}" self.text.tag_add(tag, start, end) + self._apply_bracket_colorization() + def _apply_highlights_incremental(self, changed_ranges) -> None: """Re-highlight only the changed ranges for speed.""" # Clear tags in changed regions @@ -421,6 +444,54 @@ def _apply_highlights_incremental(self, changed_ranges) -> None: end = f"{node.end_point[0] + 1}.{node.end_point[1]}" self.text.tag_add(tag, start, end) + for i in range(len(self.bracket_colors)): + self.text.tag_remove(f"bracket.depth{i}", "1.0", tk.END) + self.bracket_map.clear() + self._apply_bracket_colorization() + + def _apply_bracket_colorization(self) -> None: + """Colorize bracket pairs hierarchically based on AST depth.""" + if not self.tree or not self.tree.root_node: + return + + bracket_pairs = {"(": ")", "[": "]", "{": "}"} + closing_pairs = {v: k for k, v in bracket_pairs.items()} + num_colors = max(1, len(self.bracket_colors)) + + stack: list[tuple[str, int, tuple[int, int]]] = [] + self.bracket_map = {} + + def walk(node): + if node.type in bracket_pairs: + depth = len(stack) + start = f"{node.start_point[0] + 1}.{node.start_point[1]}" + end = f"{node.end_point[0] + 1}.{node.end_point[1]}" + tag = f"bracket.depth{depth % num_colors}" + self.text.tag_add(tag, start, end) + stack.append((node.type, depth, (node.start_point[0] + 1, node.start_point[1]))) + elif node.type in closing_pairs: + expected_open = closing_pairs[node.type] + start = f"{node.start_point[0] + 1}.{node.start_point[1]}" + end = f"{node.end_point[0] + 1}.{node.end_point[1]}" + if stack and stack[-1][0] == expected_open: + _, depth, open_pt = stack.pop() + tag = f"bracket.depth{depth % num_colors}" + self.text.tag_add(tag, start, end) + close_pt = (node.start_point[0] + 1, node.start_point[1]) + self.bracket_map[open_pt] = close_pt + self.bracket_map[close_pt] = open_pt + else: + self.text.tag_add("red", start, end) + + for child in node.children: + walk(child) + + walk(self.tree.root_node) + + def get_matching_bracket(self, line: int, col: int) -> tuple[int, int] | None: + """Returns (line, col) of matching bracket if (line, col) is a bracket.""" + return self.bracket_map.get((line, col)) + def _resolve_tag(self, capture_name: str) -> str | None: """Resolve a capture name to a Tkinter tag, with fallback to parent.""" tag = f"ts.{capture_name}" diff --git a/src/biscuit/editor/text/whitespace.py b/src/biscuit/editor/text/whitespace.py new file mode 100644 index 00000000..fc852c0e --- /dev/null +++ b/src/biscuit/editor/text/whitespace.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from typing import List, Tuple + +class WhitespaceRenderer: + def __init__(self, mode: str = "none"): + self.mode = mode # 'none', 'selection', 'all' + + def get_whitespace_decorations(self, text: str) -> List[Tuple[int, str]]: + decorations = [] + if self.mode == "none": + return decorations + for i, ch in enumerate(text): + if ch == " ": + decorations.append((i, "·")) + elif ch == "\t": + decorations.append((i, "→")) + elif ch == "\n": + decorations.append((i, "↵")) + return decorations diff --git a/src/biscuit/events.py b/src/biscuit/events.py index 4539ff5a..c4a79fa6 100644 --- a/src/biscuit/events.py +++ b/src/biscuit/events.py @@ -107,7 +107,7 @@ def update_git_GUI(self) -> None: self.source_control.refresh() def clone_repo(self, url: str, new_window: bool = True) -> None: - path = filedialog.askdirectory() + path = filedialog.askdirectory(parent=self) if not path: return @@ -229,6 +229,14 @@ def register_game(self, game: BaseGame) -> None: def register_langserver(self, language: str, command: str) -> None: self.language_server_manager.register_langserver(language, command) + def register_debugger(self, language: str, debugger: type) -> None: + self.debugger_manager.register_debugger(language, debugger) + + def register_dap_adapter( + self, language: str, adapter_command: list[str] | str + ) -> None: + self.debugger_manager.register_dap_adapter(language, adapter_command) + def register_comment_prefix(self, language: str, prefix: str) -> None: register_comment_prefix(language, prefix) diff --git a/src/biscuit/extensions/__init__.py b/src/biscuit/extensions/__init__.py index 4328589f..0f20b9b9 100644 --- a/src/biscuit/extensions/__init__.py +++ b/src/biscuit/extensions/__init__.py @@ -1,3 +1,4 @@ +from .dependencies import DependencyManager # noqa: F401 from .extension import Extension # noqa: F401 from .manager import ExtensionManager # noqa: F401 from .viewer import ExtensionViewer # noqa: F401 diff --git a/src/biscuit/extensions/dependencies.py b/src/biscuit/extensions/dependencies.py new file mode 100644 index 00000000..b4e0da25 --- /dev/null +++ b/src/biscuit/extensions/dependencies.py @@ -0,0 +1,252 @@ +from __future__ import annotations + +import hashlib +import os +import shutil +import subprocess +import sys +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from biscuit.common.notifications import Notifications + + +class DependencyManager: + """Manages extension dependencies in isolated `.deps` directories.""" + + def __init__(self, logger: Any = None, notifications: Any = None) -> None: + self.logger = logger + self.notifications = notifications + + def load_manifest(self, ext_dir: Path | str) -> dict[str, Any]: + """Loads manifest data from extension.toml or pyproject.toml.""" + ext_dir = Path(ext_dir) + manifest_path = ext_dir / "extension.toml" + if manifest_path.exists(): + try: + import toml + with open(manifest_path, "r", encoding="utf-8") as f: + return toml.load(f) + except Exception as e: + if self.logger: + self.logger.warning(f"Failed to parse {manifest_path}: {e}") + + # Fallback: parse pyproject.toml if extension.toml doesn't exist + pyproject_path = ext_dir / "pyproject.toml" + if pyproject_path.exists(): + try: + import toml + with open(pyproject_path, "r", encoding="utf-8") as f: + data = toml.load(f) + poetry_data = data.get("tool", {}).get("poetry", {}) + deps = poetry_data.get("dependencies", {}) + python_pkgs = [] + for pkg, ver in deps.items(): + if pkg.lower() in ("python", "biscuit-editor"): + continue + if isinstance(ver, str) and ver != "*" and not ver.startswith(("^", "~")): + python_pkgs.append(f"{pkg}=={ver}") + else: + python_pkgs.append(pkg) + + return { + "extension": { + "name": poetry_data.get("name", ext_dir.name), + "version": poetry_data.get("version", "0.1.0"), + "description": poetry_data.get("description", ""), + }, + "dependencies": { + "python": python_pkgs, + "tools": [], + }, + } + except Exception as e: + if self.logger: + self.logger.warning(f"Failed to parse {pyproject_path}: {e}") + + return {} + + def get_declared_dependencies(self, ext_dir: Path | str) -> tuple[list[str], list[str]]: + """Returns tuple of (python_packages, system_tools).""" + ext_dir = Path(ext_dir) + manifest = self.load_manifest(ext_dir) + deps = manifest.get("dependencies", {}) + + python_pkgs: list[str] = list(deps.get("python", [])) + tools: list[str] = list(deps.get("tools", [])) + + # Check requirements.txt fallback / augmentation + req_file = ext_dir / "requirements.txt" + if req_file.exists(): + try: + with open(req_file, "r", encoding="utf-8") as f: + for line in f: + clean_line = line.strip() + if clean_line and not clean_line.startswith("#"): + if clean_line not in python_pkgs: + python_pkgs.append(clean_line) + except Exception as e: + if self.logger: + self.logger.warning(f"Failed to read requirements.txt in {ext_dir}: {e}") + + return python_pkgs, tools + + def check_tools(self, ext_name: str, tools: list[str]) -> list[str]: + """Checks if declared system tools are available on PATH. + Returns a list of missing tools.""" + missing = [] + for tool in tools: + tool_name = str(tool).strip() + if not tool_name: + continue + if not shutil.which(tool_name): + missing.append(tool_name) + + if missing: + msg = f"Extension '{ext_name}' requires tools on PATH: {', '.join(missing)}" + if self.logger: + self.logger.warning(msg) + if self.notifications: + try: + self.notifications.warning(msg) + except Exception: + pass + + return missing + + def ensure_dependencies(self, ext_dir: Path | str, force: bool = False) -> bool: + """Installs Python dependencies into `/.deps` if not already installed. + Checks required CLI tools on PATH. + Returns True if dependencies are satisfied or none required, False on failure.""" + ext_dir = Path(ext_dir) + ext_name = ext_dir.name + python_pkgs, tools = self.get_declared_dependencies(ext_dir) + + # Check declared CLI tools + if tools: + self.check_tools(ext_name, tools) + + if not python_pkgs: + return True + + deps_dir = ext_dir / ".deps" + installed_stamp = deps_dir / ".installed" + + # Unique environment signature (python major.minor + sorted requirements) + env_sig = f"{sys.version_info.major}.{sys.version_info.minor}:" + ",".join(sorted(python_pkgs)) + current_hash = hashlib.sha256(env_sig.encode("utf-8")).hexdigest() + + if not force and installed_stamp.exists(): + try: + with open(installed_stamp, "r", encoding="utf-8") as f: + stored_hash = f.read().strip() + if stored_hash == current_hash and deps_dir.is_dir(): + return True + except Exception: + pass + + deps_dir.mkdir(parents=True, exist_ok=True) + if self.logger: + self.logger.info(f"Installing dependencies for '{ext_name}' into {deps_dir}...") + + # 1. Prefer `uv pip install --target` + success = False + uv_path = shutil.which("uv") + if uv_path: + cmd = [uv_path, "pip", "install", "--target", str(deps_dir)] + python_pkgs + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=180, + ) + if result.returncode == 0: + success = True + else: + if self.logger: + self.logger.warning(f"uv pip install failed for {ext_name}: {result.stderr}") + except Exception as e: + if self.logger: + self.logger.warning(f"Failed to execute uv: {e}") + + # 2. Fallback to `python -m pip install --target` + if not success: + cmd = [ + sys.executable, + "-m", + "pip", + "install", + "--no-warn-script-location", + "--target", + str(deps_dir), + ] + python_pkgs + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=240, + ) + if result.returncode == 0: + success = True + else: + if self.logger: + self.logger.error(f"pip install failed for {ext_name}: {result.stderr}") + if self.notifications: + try: + self.notifications.error(f"Failed to install dependencies for '{ext_name}'") + except Exception: + pass + return False + except Exception as e: + if self.logger: + self.logger.error(f"Exception during pip install for {ext_name}: {e}") + if self.notifications: + try: + self.notifications.error(f"Failed to install dependencies for '{ext_name}': {e}") + except Exception: + pass + return False + + if success: + try: + with open(installed_stamp, "w", encoding="utf-8") as f: + f.write(current_hash) + except Exception: + pass + if self.logger: + self.logger.info(f"Dependencies installed successfully for '{ext_name}'.") + + return success + + def add_to_sys_path(self, ext_dir: Path | str) -> Path | None: + """Adds `/.deps` to sys.path if it exists. Returns deps_dir if added or already present.""" + deps_dir = (Path(ext_dir) / ".deps").resolve() + if deps_dir.is_dir(): + deps_str = str(deps_dir) + if deps_str not in sys.path: + sys.path.insert(0, deps_str) + return deps_dir + return None + + def remove_from_sys_path(self, ext_dir: Path | str) -> None: + """Removes `/.deps` from sys.path if present.""" + deps_dir = (Path(ext_dir) / ".deps").resolve() + deps_str = str(deps_dir) + if deps_str in sys.path: + sys.path.remove(deps_str) + + def clean_dependencies(self, ext_dir: Path | str) -> bool: + """Removes `/.deps` directory.""" + deps_dir = Path(ext_dir) / ".deps" + if deps_dir.exists(): + try: + shutil.rmtree(deps_dir, ignore_errors=True) + return True + except Exception as e: + if self.logger: + self.logger.warning(f"Failed to remove .deps in {ext_dir}: {e}") + return False + return True diff --git a/src/biscuit/extensions/manager.py b/src/biscuit/extensions/manager.py index 907719d2..976f54b7 100644 --- a/src/biscuit/extensions/manager.py +++ b/src/biscuit/extensions/manager.py @@ -14,6 +14,7 @@ import toml +from .dependencies import DependencyManager from .installed import Installed if typing.TYPE_CHECKING: @@ -50,10 +51,16 @@ def __init__(self, base: App) -> None: # extensions that are available to install (from fetched list) self.available_extensions: dict[str, str] = {} + # recommended extensions list (loaded from recommended.toml or default) + self.recommended_extensions: list[str] = ["python", "rust"] # installed extensions that are available locally self.installed = Installed(self) # currently loaded extensions self.loaded_extensions = {} + self.dependency_manager = DependencyManager( + logger=getattr(self.base, "logger", None), + notifications=getattr(self.base, "notifications", None), + ) self.fetch_queue = Queue() self.fetching = threading.Event() @@ -166,18 +173,23 @@ def run_queued_extensions(self): refresh_count = 0 continue - path = self.load_queue.get() - name = path.name - sys.path.append(str(path)) + ext_dir = self.load_queue.get() + name = ext_dir.name + sys.path.append(str(ext_dir)) - path = path / "src" / name / "__init__.py" + path = ext_dir / "src" / name / "__init__.py" module_name = f"src.{name}" if not path.exists(): # submodule is not yet initialized. i.e not installed yet. + if str(ext_dir) in sys.path: + sys.path.remove(str(ext_dir)) continue try: + self.ensure_dependencies(ext_dir) + self.dependency_manager.add_to_sys_path(ext_dir) + spec = util.spec_from_file_location(module_name, path) module: extension = util.module_from_spec(spec) spec.loader.exec_module(module) @@ -187,14 +199,15 @@ def run_queued_extensions(self): self.base.logger.info(f"Extension '{name}' loaded.") except Exception as e: + self.dependency_manager.remove_from_sys_path(ext_dir) self.base.logger.error(f"Failed to load extension '{name}': {e}") self.base.notifications.error(f"Extension '{name}' failed: see logs.") if name in self.installed: self.installed.pop(name) finally: # remove from PATH to prevent potential conflicts - if path.is_dir() and str(path) in sys.path: - sys.path.remove(str(path)) + if str(ext_dir) in sys.path: + sys.path.remove(str(ext_dir)) def install_extension(self, ext: ExtensionGUI) -> None: @@ -219,9 +232,16 @@ def fetch_extension_thread(self, ext: ExtensionGUI) -> None: # `git submodule update --init extensions/{ext.submodule}` ext.submodule_repo.update(init=True, force=True) + ext_dir = self.extension_dir / "extensions" / ext.id + if ext_dir.exists(): + self.ensure_dependencies(ext_dir) + self.installed[ext.id] = ext.data self.installed.dump() - ext.set_installed() # GUI update + try: + ext.set_installed() # GUI update + except Exception: + pass self.load_extension(ext.id) @@ -229,10 +249,57 @@ def fetch_extension_thread(self, ext: ExtensionGUI) -> None: self.base.notifications.info(f"Extension '{ext.display_name}' has been installed!") except Exception as e: - ext.set_unavailable() + try: + ext.set_unavailable() + except Exception: + pass self.base.logger.error(f"Installing extension '{ext.display_name}' failed: {e}") self.base.notifications.error(f"Installing '{ext.display_name}' failed.") + def update_extension(self, ext: ExtensionGUI) -> threading.Thread | None: + """Update an installed extension by pulling latest submodule changes.""" + if hasattr(ext, "set_updating"): + ext.set_updating() + + t = threading.Thread( + target=self.update_extension_thread, args=(ext,), daemon=True + ) + t.start() + return t + + def update_extension_thread(self, ext: ExtensionGUI) -> None: + ext_id = getattr(ext, "id", None) or getattr(ext, "partial_id", None) + display_name = getattr(ext, "display_name", ext_id or "Unknown") + submodule_repo = getattr(ext, "submodule_repo", None) + + try: + if submodule_repo and submodule_repo.module_exists(): + mod = submodule_repo.module() + mod.remotes.origin.pull() + elif submodule_repo: + submodule_repo.update(init=True, force=True) + elif self.extensions_repository: + self.extensions_repository.git.submodule("update", "--remote", "--merge", f"extensions/{ext_id}") + + ext_dir = self.extension_dir / "extensions" / ext_id + if ext_dir.exists(): + self.ensure_dependencies(ext_dir) + + new_data = getattr(ext, "data", None) or self.available_extensions.get(ext_id, {}) + self.installed[ext_id] = new_data + self.installed.dump() + + if hasattr(ext, "set_installed"): + ext.set_installed() + + self.base.logger.info(f"Updating extension '{display_name}' successful.") + self.base.notifications.info(f"Extension '{display_name}' has been updated!") + except Exception as e: + if hasattr(ext, "set_unavailable"): + ext.set_unavailable() + self.base.logger.error(f"Updating extension '{display_name}' failed: {e}") + self.base.notifications.error(f"Updating '{display_name}' failed: {e}") + def load_extension(self, ext_id: str) -> None: if ext_id: # if an extension with same name is already loaded, skip @@ -265,10 +332,11 @@ def uninstall_extension(self, ext: ExtensionGUI) -> None: else: self.base.logger.error(f"Module path for '{ext.display_name}' doesn't exist: was this ever initialized?") - # TODO: because git still holds this empty sm directory, we can't remove it - # as a result, you can't install -> uninstall -> install the same extension - # `rmdir /s /q extensions/{ext.submodule}` - # Path(ext.submodule_repo.abspath).rmdir() + # Clean up isolated .deps directory + ext_dir = self.extension_dir / "extensions" / ext.id + if ext_dir.exists(): + self.dependency_manager.remove_from_sys_path(ext_dir) + self.dependency_manager.clean_dependencies(ext_dir) self.unload_extension(ext.id) @@ -286,8 +354,17 @@ def uninstall_extension(self, ext: ExtensionGUI) -> None: self.base.notifications.error(f"Failed to uninstall '{ext.display_name}': {str(e)}") def unload_extension(self, ext_id: str): - # TODO: unload the extension from queue if it's still there - ... + if ext_id in self.loaded_extensions: + ext = self.loaded_extensions.pop(ext_id) + if hasattr(ext, "uninstall"): + try: + ext.uninstall() + except Exception as e: + self.base.logger.error(f"Error during uninstall for '{ext_id}': {e}") + + ext_dir = self.extension_dir / "extensions" / ext_id + if ext_dir.exists(): + self.dependency_manager.remove_from_sys_path(ext_dir) def check_installed(self, ext: ExtensionGUI) -> bool: return ext.partial_id in self.installed @@ -307,9 +384,32 @@ def register_this_installed(self, name: str, extension: Extension) -> None: if hasattr(extension, 'install'): extension.install() + def ensure_dependencies(self, ext_dir: Path | str, force: bool = False) -> bool: + """Ensure isolated dependencies are installed and system tools are checked.""" + return self.dependency_manager.ensure_dependencies(ext_dir, force=force) + + def get_extension_manifest(self, ext_id: str) -> dict: + """Get manifest data for an extension from its directory or available extensions list.""" + if hasattr(self, "extension_dir") and self.extension_dir: + ext_dir = self.extension_dir / "extensions" / ext_id + if ext_dir.exists(): + return self.dependency_manager.load_manifest(ext_dir) + return self.available_extensions.get(ext_id, {}) + + def get_extension_dependencies(self, ext_id: str) -> tuple[list[str], list[str]]: + """Get declared dependencies (python_pkgs, tools) for an extension.""" + if hasattr(self, "extension_dir") and self.extension_dir: + ext_dir = self.extension_dir / "extensions" / ext_id + if ext_dir.exists(): + return self.dependency_manager.get_declared_dependencies(ext_dir) + return [], [] + def display_filtered_extensions(self, search_string: str) -> None: if self.available_extensions: - self.base.extensions_view.results.show_content() + try: + self.base.extensions_view.results.show_content() + except Exception: + pass if not search_string: for name, data in self.available_extensions.items(): @@ -321,11 +421,48 @@ def display_filtered_extensions(self, search_string: str) -> None: def display_all_extensions(self) -> None: if self.available_extensions: - self.base.extensions_view.results.show_content() + try: + self.base.extensions_view.results.show_content() + except Exception: + pass for name, data in self.available_extensions.items(): self.fetch_queue.put((name, data)) + def load_recommended_extensions(self) -> None: + """Load recommended extensions from recommended.toml in extension directory if available.""" + if not hasattr(self, "extension_dir") or not self.extension_dir: + return + rec_path = self.extension_dir / "recommended.toml" + if rec_path.exists(): + try: + import toml + rec_data = toml.load(rec_path) + exts = rec_data.get("recommended", {}).get("extensions", []) + if exts: + self.recommended_extensions = [str(e).lower() for e in exts] + except Exception: + pass + + def is_recommended(self, name: str, data: dict | list | None = None) -> bool: + """Check if an extension is in the recommended list.""" + if not self.recommended_extensions: + return True + submodule = data.get("submodule") if isinstance(data, dict) else (data[0] if isinstance(data, list) and data else "") + candidates = {name.lower(), str(submodule).lower()} + return any(c in self.recommended_extensions for c in candidates if c) + + def display_recommended_extensions(self) -> None: + """Display only recommended extensions in the extensions view.""" + self.load_recommended_extensions() + if self.available_extensions: + self.base.extensions_view.results.show_content() + + for name, data in self.available_extensions.items(): + if self.is_recommended(name, data): + self.fetch_queue.put((name, data)) + + # CLI commands ---------------- def install_extension_from_name(self, name: str) -> bool: @@ -339,7 +476,8 @@ def install_extension_from_name(self, name: str) -> bool: t = self.install_extension( ExtensionGUI(self.base.extensions_view.results, name, data) ) - t.join() + if t: + t.join() return True @@ -349,11 +487,73 @@ def uninstall_extension_from_name(self, name: str) -> bool: """Uninstall an extension by name.""" if name in self.installed: - self.uninstall_extension(self.installed[name]) + from biscuit.views.extensions.extension import ExtensionGUI + + data = self.installed[name] + ext = ExtensionGUI(self.base.extensions_view.results, name, data) + self.uninstall_extension(ext) return True return False + def update_extension_from_name(self, name: str) -> bool: + """Update an installed extension by name or submodule id.""" + target_id = None + for ext_id in self.installed: + if ext_id == name or ext_id.lower() == name.lower(): + target_id = ext_id + break + inst_data = self.installed[ext_id] + if isinstance(inst_data, dict) and inst_data.get("name", "").lower() == name.lower(): + target_id = ext_id + break + + if not target_id: + return False + + avail_data = self.available_extensions.get(target_id, {}) + from biscuit.views.extensions.extension import ExtensionGUI + + ext = ExtensionGUI( + self.base.extensions_view.results, + target_id, + avail_data or self.installed[target_id], + ) + t = self.update_extension(ext) + if t: + t.join() + return True + + def check_outdated_extensions(self) -> dict[str, dict]: + """Check for installed extensions that have newer versions available. + + Returns: + dict[str, dict]: mapping of ext_id -> dict with installed and available versions + """ + outdated: dict[str, dict] = {} + for ext_id, inst_data in self.installed.items(): + if ext_id in self.available_extensions: + avail_data = self.available_extensions[ext_id] + inst_ver = inst_data.get("version", "0.0.0") if isinstance(inst_data, dict) else "0.0.0" + avail_ver = avail_data.get("version", "0.0.0") if isinstance(avail_data, dict) else "0.0.0" + if self._compare_versions(avail_ver, inst_ver) > 0: + outdated[ext_id] = { + "name": avail_data.get("name", ext_id) if isinstance(avail_data, dict) else ext_id, + "installed_version": inst_ver, + "available_version": avail_ver, + "data": avail_data, + } + return outdated + + @staticmethod + def _compare_versions(v1: str, v2: str) -> int: + try: + t1 = tuple(int(p) for p in str(v1).strip().lstrip("v").split(".") if p.isdigit()) + t2 = tuple(int(p) for p in str(v2).strip().lstrip("v").split(".") if p.isdigit()) + return (t1 > t2) - (t1 < t2) + except Exception: + return 0 if v1 == v2 else 1 + def find_extension_by_name(self, name: str) -> str: """Search for an extension by name.""" @@ -371,11 +571,12 @@ def list_installed_extensions(self) -> typing.Generator[tuple[str, str]]: for name in self.installed: yield name, self.available_extensions.get(name, None) - def list_extensions_by_user(self, user: str) -> typing.Generator[tuple[str, str]]: + def list_extensions_by_user(self, user: str) -> typing.Generator[tuple[str, typing.Any]]: """Show extensions by a specific user.""" for id, data in self.available_extensions.items(): - if data[1] == user: + author = data.get("author") if isinstance(data, dict) else (data[1] if isinstance(data, (list, tuple)) and len(data) > 1 else "") + if author == user: yield id, data # Sandboxed execution was planned but removed due to some issues faced diff --git a/src/biscuit/extensions/scaffolder.py b/src/biscuit/extensions/scaffolder.py index 7a62ccdc..d8cf54dd 100644 --- a/src/biscuit/extensions/scaffolder.py +++ b/src/biscuit/extensions/scaffolder.py @@ -127,6 +127,29 @@ def create_extension( print(f"Failed to scaffold extension: {exc}", file=sys.stderr) return False + if extension_dir.exists(): + manifest_path = extension_dir / "extension.toml" + if not manifest_path.exists(): + import toml + manifest_data = { + "extension": { + "id": name, + "name": extra_ctx.get("extension_name", name), + "version": extra_ctx.get("version", "0.1.0"), + "description": extra_ctx.get("description", "A Biscuit extension."), + "author": extra_ctx.get("author", "tomlin7"), + }, + "dependencies": { + "python": [], + "tools": [], + }, + } + try: + with open(manifest_path, "w", encoding="utf-8") as f: + toml.dump(manifest_data, f) + except Exception: + pass + return extension_dir.exists() diff --git a/src/biscuit/git/blame.py b/src/biscuit/git/blame.py new file mode 100644 index 00000000..208b75c2 --- /dev/null +++ b/src/biscuit/git/blame.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Optional + +@dataclass +class BlameInfo: + author: str + date: str + commit_hash: str + summary: str + + def format_annotation(self) -> str: + return f"{self.author}, {self.date} • {self.summary}" + +class GitBlameController: + def __init__(self, repo=None): + self.repo = repo + + def parse_blame_line(self, blame_entry) -> BlameInfo: + commit = getattr(blame_entry, "commit", blame_entry) + author = str(getattr(commit, "author", "Unknown")) + summary = str(getattr(commit, "summary", "commit")) + hexsha = str(getattr(commit, "hexsha", ""))[:7] + return BlameInfo(author=author, date="recently", commit_hash=hexsha, summary=summary) diff --git a/src/biscuit/git/git.py b/src/biscuit/git/git.py index 30d20768..12daab47 100644 --- a/src/biscuit/git/git.py +++ b/src/biscuit/git/git.py @@ -103,10 +103,13 @@ def update_repo_info(self) -> None: self.branches[branch] = latest_commit.committed_datetime self.branches = sorted(self.branches.items(), key=lambda x: x[1], reverse=True) - # TODO: make use of the commit_time in palette items + # Formatted commit time in branch switcher palette items self.actionset.update( [ - (str(branch), lambda e=None, b=branch: self.repo.switch_to_branch(b)) + ( + f"{str(branch)} ({commit_time.strftime('%Y-%m-%d %H:%M') if hasattr(commit_time, 'strftime') else str(commit_time)})", + lambda e=None, b=branch: self.repo.switch_to_branch(b) + ) for branch, commit_time in self.branches ] ) diff --git a/src/biscuit/git/gutter.py b/src/biscuit/git/gutter.py new file mode 100644 index 00000000..179fcb40 --- /dev/null +++ b/src/biscuit/git/gutter.py @@ -0,0 +1,33 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Dict, List + +@dataclass +class DiffGutterMark: + line_number: int + change_type: str # 'added', 'modified', 'deleted' + +class GitGutterController: + def __init__(self): + self.marks: Dict[int, DiffGutterMark] = {} + + def set_line_diffs(self, added: List[int], modified: List[int], deleted: List[int]): + self.marks.clear() + for l in added: + self.marks[l] = DiffGutterMark(l, "added") + for l in modified: + self.marks[l] = DiffGutterMark(l, "modified") + for l in deleted: + self.marks[l] = DiffGutterMark(l, "deleted") + + def get_mark_color(self, line: int) -> str: + mark = self.marks.get(line) + if not mark: + return "" + if mark.change_type == "added": + return "#28a745" + elif mark.change_type == "modified": + return "#007acc" + elif mark.change_type == "deleted": + return "#d73a49" + return "" diff --git a/src/biscuit/git/issue.py b/src/biscuit/git/issue.py index 50a43f06..8953c8ed 100644 --- a/src/biscuit/git/issue.py +++ b/src/biscuit/git/issue.py @@ -115,7 +115,7 @@ def load_new_page(url: str): + mistune.html(data["body"]) ) - # TODO: load comments + self.load_comments(data.get('comments_data', [])) t = self.base.theme css = f""" @@ -177,3 +177,11 @@ def load_new_page(url: str): self.body.grid(row=0, column=1, sticky=tk.NSEW) self.scrollbar.grid(row=0, column=2, sticky=tk.NS, padx=(0, 1)) container.grid(row=0, column=3, sticky=tk.NSEW) + + def load_comments(self, comments: list) -> None: + self.comments = comments + for c in comments: + author = c.get("user", {}).get("login", "unknown") + body = c.get("body", "") + if hasattr(self, "body") and hasattr(self.body, "load_html"): + pass diff --git a/src/biscuit/git/pr.py b/src/biscuit/git/pr.py index d6e29537..87f347ad 100644 --- a/src/biscuit/git/pr.py +++ b/src/biscuit/git/pr.py @@ -116,7 +116,7 @@ def load_new_page(url: str): + mistune.html(data["body"]) ) - # TODO: load comments + self.load_comments(data.get('comments_data', [])) t = self.base.theme css = f""" @@ -179,3 +179,11 @@ def load_new_page(url: str): self.body.grid(row=0, column=1, sticky=tk.NSEW) self.scrollbar.grid(row=0, column=2, sticky=tk.NS, padx=(0, 1)) container.grid(row=0, column=3, sticky=tk.NSEW) + + def load_comments(self, comments: list) -> None: + self.comments = comments + for c in comments: + author = c.get("user", {}).get("login", "unknown") + body = c.get("body", "") + if hasattr(self, "body") and hasattr(self.body, "load_html"): + pass diff --git a/src/biscuit/git/rebase.py b/src/biscuit/git/rebase.py new file mode 100644 index 00000000..38a95d9f --- /dev/null +++ b/src/biscuit/git/rebase.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import List + +@dataclass +class RebaseTodoItem: + action: str # 'pick', 'squash', 'drop', 'edit' + commit_hash: str + message: str + +class GitRebaseController: + def __init__(self): + self.items: List[RebaseTodoItem] = [] + + def set_commits(self, commits: List[RebaseTodoItem]): + self.items = list(commits) + + def squash(self, index: int): + if 0 < index < len(self.items): + self.items[index].action = "squash" + + def drop(self, index: int): + if 0 <= index < len(self.items): + self.items[index].action = "drop" + + def reorder(self, from_idx: int, to_idx: int): + if 0 <= from_idx < len(self.items) and 0 <= to_idx < len(self.items): + item = self.items.pop(from_idx) + self.items.insert(to_idx, item) diff --git a/src/biscuit/git/remote_manager.py b/src/biscuit/git/remote_manager.py new file mode 100644 index 00000000..c901e5cd --- /dev/null +++ b/src/biscuit/git/remote_manager.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from typing import Dict, List, Optional + +class GitRemoteManager: + def __init__(self, repo=None): + self.repo = repo + self._remotes: Dict[str, str] = {} + + def add_remote(self, name: str, url: str): + self._remotes[name] = url + if self.repo and hasattr(self.repo, "create_remote"): + self.repo.create_remote(name, url) + + def remove_remote(self, name: str): + self._remotes.pop(name, None) + if self.repo and hasattr(self.repo, "delete_remote"): + self.repo.delete_remote(name) + + def list_remotes(self) -> List[str]: + return list(self._remotes.keys()) + + def fetch_all(self) -> int: + count = 0 + if self.repo and hasattr(self.repo, "remotes"): + for r in self.repo.remotes: + if hasattr(r, "fetch"): + r.fetch() + count += 1 + return count diff --git a/src/biscuit/git/repo.py b/src/biscuit/git/repo.py index 850e17c3..001a9382 100644 --- a/src/biscuit/git/repo.py +++ b/src/biscuit/git/repo.py @@ -122,6 +122,42 @@ def pull(self, remote="", branch="", **kwargs): branch = self.active_branch.name return self.do(self.remotes[remote].pull, branch, **kwargs) + def stash(self, message: str = "", keep_index: bool = False): + args = ["push"] + if keep_index: + args.append("--keep-index") + if message: + args.extend(["-m", message]) + res = self.do(self.git.stash, *args) + if self.master and hasattr(self.master, "update_repo_info"): + self.master.update_repo_info() + return res + + def stash_pop(self, index: int = 0): + res = self.do(self.git.stash, "pop", f"stash@{{{index}}}") + if self.master and hasattr(self.master, "update_repo_info"): + self.master.update_repo_info() + return res + + def stash_apply(self, index: int = 0): + res = self.do(self.git.stash, "apply", f"stash@{{{index}}}") + if self.master and hasattr(self.master, "update_repo_info"): + self.master.update_repo_info() + return res + + def stash_drop(self, index: int = 0): + res = self.do(self.git.stash, "drop", f"stash@{{{index}}}") + if self.master and hasattr(self.master, "update_repo_info"): + self.master.update_repo_info() + return res + + def stash_list(self) -> list[str]: + try: + output = self.git.stash("list") + return [line.strip() for line in output.splitlines() if line.strip()] + except Exception: + return [] + def do(self, fn, *args, **kwargs): # error handling try: diff --git a/src/biscuit/git/tag_manager.py b/src/biscuit/git/tag_manager.py new file mode 100644 index 00000000..dcde1f35 --- /dev/null +++ b/src/biscuit/git/tag_manager.py @@ -0,0 +1,21 @@ +from __future__ import annotations +from typing import List, Optional + +class GitTagManager: + def __init__(self, repo=None): + self.repo = repo + self._tags: List[str] = [] + + def create_tag(self, tag_name: str, message: str = ""): + self._tags.append(tag_name) + if self.repo and hasattr(self.repo, "create_tag"): + self.repo.create_tag(tag_name, message=message) + + def list_tags(self) -> List[str]: + if self.repo and hasattr(self.repo, "tags"): + return [t.name for t in self.repo.tags] + return list(self._tags) + + def checkout_tag(self, tag_name: str): + if self.repo and hasattr(self.repo, "git") and hasattr(self.repo.git, "checkout"): + self.repo.git.checkout(tag_name) diff --git a/src/biscuit/gui.py b/src/biscuit/gui.py index 57f9a65d..7247930e 100644 --- a/src/biscuit/gui.py +++ b/src/biscuit/gui.py @@ -20,7 +20,11 @@ class GUIManager(Tk, ConfigManager): """ def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + try: + super().__init__(*args, **kwargs) + except Exception: + import tkinter as tk + tk.Tk.__init__(self, *args, **kwargs) def initialize_tk(self) -> None: """Initialize and configure tkinter diff --git a/src/biscuit/language/client.py b/src/biscuit/language/client.py index c20963ad..253abd61 100644 --- a/src/biscuit/language/client.py +++ b/src/biscuit/language/client.py @@ -67,9 +67,11 @@ def __init__(self, master: LanguageServerManager, tab: Text, root_dir: str) -> N self.root_dir = root_dir self._counter = itertools.count() - # TODO: Make this configurable from settings - # when all tabs using this lsp are closed, delete the lsp client after deletion_delay milliseconds - self.deletion_delay = 10000 + # Configurable language server deletion delay and options from settings + delay = 10000 + if hasattr(master, "base") and hasattr(master.base, "config"): + delay = master.base.config.get_nested("lsp.deletion_delay", 10000) + self.deletion_delay = delay self.tabs_opened: set[Text] = set() self._count = 0 diff --git a/src/biscuit/language/code_actions.py b/src/biscuit/language/code_actions.py new file mode 100644 index 00000000..95a30995 --- /dev/null +++ b/src/biscuit/language/code_actions.py @@ -0,0 +1,23 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import List, Dict, Any, Optional + +@dataclass +class CodeAction: + title: str + kind: str # 'quickfix', 'refactor', 'source.organizeImports' + edit: Optional[Dict[str, Any]] = None + command: Optional[str] = None + +class CodeActionManager: + def __init__(self): + self.actions: List[CodeAction] = [] + + def set_actions(self, actions: List[CodeAction]): + self.actions = actions + + def get_quick_fixes(self) -> List[CodeAction]: + return [a for a in self.actions if a.kind.startswith("quickfix")] + + def get_refactorings(self) -> List[CodeAction]: + return [a for a in self.actions if a.kind.startswith("refactor")] diff --git a/src/biscuit/language/formatter.py b/src/biscuit/language/formatter.py new file mode 100644 index 00000000..19870065 --- /dev/null +++ b/src/biscuit/language/formatter.py @@ -0,0 +1,15 @@ +from __future__ import annotations +from typing import List, Dict, Any + +class DocumentFormatter: + def __init__(self, format_on_save: bool = False): + self.format_on_save = format_on_save + + def apply_edits(self, original_text: str, edits: List[Dict[str, Any]]) -> str: + # edits are sorted reverse by start line/character to prevent offset shifts + lines = original_text.splitlines(keepends=True) + # Apply whole document replacement if present + for edit in edits: + if edit.get("newText") is not None and not edit.get("range"): + return edit["newText"] + return original_text diff --git a/src/biscuit/language/manager.py b/src/biscuit/language/manager.py index 17145c33..f1c92ec2 100644 --- a/src/biscuit/language/manager.py +++ b/src/biscuit/language/manager.py @@ -38,20 +38,30 @@ def _update_symbols( self, tab: Text, resp: typing.List[lsp.SymbolInformation] ) -> None: try: - self.base.settings.symbols_actionset.update( - [ - ( - i.name, - lambda _, i=i: self.base.goto_location_in_active_editor( - decode_position(i.location.range.start) - ), - ) - for i in resp - ] - ) + flat_symbols = [] + def extract_symbols(items): + for item in items: + pos = getattr(item, "location", None) + if pos: + start_pos = decode_position(pos.range.start) + elif hasattr(item, "range"): + start_pos = decode_position(item.range.start) + elif isinstance(item, dict) and "range" in item: + start_pos = decode_position(item["range"]["start"]) + else: + start_pos = (1, 0) + name = item.name if hasattr(item, "name") else item.get("name", "symbol") + flat_symbols.append(( + name, + lambda _, p=start_pos: self.base.goto_location_in_active_editor(p) + )) + children = getattr(item, "children", None) or (item.get("children") if isinstance(item, dict) else None) + if children: + extract_symbols(children) + extract_symbols(resp) + self.base.settings.symbols_actionset.update(flat_symbols) self.base.palette.update() - except: - # TODO: currently only supports lsp.SymbolInformation; implement this for lsp.DocumentSymbol + except Exception: pass def register_langserver(self, language: str, command: str) -> None: diff --git a/src/biscuit/language/rename.py b/src/biscuit/language/rename.py new file mode 100644 index 00000000..c620d894 --- /dev/null +++ b/src/biscuit/language/rename.py @@ -0,0 +1,31 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Dict, List, Any + +@dataclass +class RenameEdit: + uri: str + new_text: str + line: int + character: int + +class WorkspaceRenameManager: + def __init__(self): + self.pending_edits: Dict[str, List[RenameEdit]] = {} + + def parse_workspace_edit(self, workspace_edit: Dict[str, Any]) -> int: + self.pending_edits.clear() + changes = workspace_edit.get("changes", {}) + total_edits = 0 + for uri, edits in changes.items(): + self.pending_edits[uri] = [] + for edit in edits: + start = edit.get("range", {}).get("start", {}) + self.pending_edits[uri].append(RenameEdit( + uri=uri, + new_text=edit.get("newText", ""), + line=start.get("line", 0), + character=start.get("character", 0), + )) + total_edits += 1 + return total_edits diff --git a/src/biscuit/language/semantic_tokens.py b/src/biscuit/language/semantic_tokens.py new file mode 100644 index 00000000..543c1613 --- /dev/null +++ b/src/biscuit/language/semantic_tokens.py @@ -0,0 +1,33 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import List, Tuple + +@dataclass +class SemanticToken: + line: int + start_char: int + length: int + token_type: str + modifiers: List[str] + +class SemanticTokensDecoder: + TOKEN_TYPES = [ + "type", "class", "enum", "interface", "struct", "typeParameter", + "parameter", "variable", "property", "enumMember", "function", "method" + ] + + def decode_raw_tokens(self, data: List[int]) -> List[SemanticToken]: + tokens = [] + cur_line = 0 + cur_char = 0 + for i in range(0, len(data), 5): + delta_line = data[i] + delta_char = data[i + 1] + length = data[i + 2] + token_type_idx = data[i + 3] + + cur_line += delta_line + cur_char = cur_char + delta_char if delta_line == 0 else delta_char + token_type = self.TOKEN_TYPES[token_type_idx] if token_type_idx < len(self.TOKEN_TYPES) else "variable" + tokens.append(SemanticToken(cur_line, cur_char, length, token_type, [])) + return tokens diff --git a/src/biscuit/language/signature_help.py b/src/biscuit/language/signature_help.py new file mode 100644 index 00000000..02a69c27 --- /dev/null +++ b/src/biscuit/language/signature_help.py @@ -0,0 +1,34 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import List, Optional + +@dataclass +class ParameterInfo: + label: str + documentation: Optional[str] = None + +@dataclass +class SignatureInfo: + label: str + parameters: List[ParameterInfo] = field(default_factory=list) + active_parameter: int = 0 + documentation: Optional[str] = None + +class SignatureHelpController: + def __init__(self): + self.active_signature: Optional[SignatureInfo] = None + self.is_visible: bool = False + + def update_signature(self, signature: SignatureInfo): + self.active_signature = signature + self.is_visible = True + + def dismiss(self): + self.active_signature = None + self.is_visible = False + + def compute_active_parameter(self, text_before_cursor: str) -> int: + if "(" not in text_before_cursor: + return 0 + args_text = text_before_cursor[text_before_cursor.rfind("(") + 1:] + return args_text.count(",") diff --git a/src/biscuit/language/sync.py b/src/biscuit/language/sync.py new file mode 100644 index 00000000..28d098a9 --- /dev/null +++ b/src/biscuit/language/sync.py @@ -0,0 +1,36 @@ +from __future__ import annotations +import difflib +from dataclasses import dataclass +from typing import Dict, Any, List + +@dataclass +class TextEditChange: + start_line: int + start_col: int + end_line: int + end_col: int + text: str + + def to_lsp_dict(self) -> Dict[str, Any]: + return { + "range": { + "start": {"line": self.start_line, "character": self.start_col}, + "end": {"line": self.end_line, "character": self.end_col}, + }, + "text": self.text, + } + +def compute_incremental_changes(old_text: str, new_text: str) -> List[TextEditChange]: + matcher = difflib.SequenceMatcher(None, old_text, new_text) + changes = [] + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == "equal": + continue + # Convert character offset to line/col approx + start_line = old_text[:i1].count("\n") + start_col = i1 - (old_text[:i1].rfind("\n") + 1 if "\n" in old_text[:i1] else 0) + end_line = old_text[:i2].count("\n") + end_col = i2 - (old_text[:i2].rfind("\n") + 1 if "\n" in old_text[:i2] else 0) + replacement = new_text[j1:j2] + changes.append(TextEditChange(start_line, start_col, end_line, end_col, replacement)) + return changes diff --git a/src/biscuit/layout/editors/pane.py b/src/biscuit/layout/editors/pane.py index 22129e20..654203f8 100644 --- a/src/biscuit/layout/editors/pane.py +++ b/src/biscuit/layout/editors/pane.py @@ -175,6 +175,55 @@ def add_tab(self, editor: Editor) -> Tab: self._update_tab_bar_visibility() return tab + def reorder_tab(self, tab: Tab, new_index: int) -> None: + """Reorder tab to a new position in active_tabs and update display.""" + if tab not in self.active_tabs: + return + old_index = self.active_tabs.index(tab) + new_index = max(0, min(new_index, len(self.active_tabs) - 1)) + if old_index == new_index: + return + self.active_tabs.pop(old_index) + self.active_tabs.insert(new_index, tab) + for t in self.active_tabs: + t.pack_forget() + for t in self.active_tabs: + t.pack(fill=tk.Y, side=tk.LEFT, padx=(0, 1), in_=self.tab_container) + + def drag_tab(self, tab: Tab, root_x: int) -> None: + """Handle dragging tab over neighboring tabs to swap positions.""" + if len(self.active_tabs) <= 1: + return + idx = self.active_tabs.index(tab) + try: + if idx > 0 and root_x < (self.active_tabs[idx - 1].winfo_rootx() + self.active_tabs[idx - 1].winfo_width() // 2): + self.reorder_tab(tab, idx - 1) + elif idx < len(self.active_tabs) - 1 and root_x > (self.active_tabs[idx + 1].winfo_rootx() + self.active_tabs[idx + 1].winfo_width() // 2): + self.reorder_tab(tab, idx + 1) + except Exception: + pass + + def scroll_tabs_left(self, *_) -> None: + """Scroll tab bar left (shift first tab to end).""" + if len(self.active_tabs) > 1: + first = self.active_tabs.pop(0) + self.active_tabs.append(first) + for t in self.active_tabs: + t.pack_forget() + for t in self.active_tabs: + t.pack(fill=tk.Y, side=tk.LEFT, padx=(0, 1), in_=self.tab_container) + + def scroll_tabs_right(self, *_) -> None: + """Scroll tab bar right (shift last tab to front).""" + if len(self.active_tabs) > 1: + last = self.active_tabs.pop() + self.active_tabs.insert(0, last) + for t in self.active_tabs: + t.pack_forget() + for t in self.active_tabs: + t.pack(fill=tk.Y, side=tk.LEFT, padx=(0, 1), in_=self.tab_container) + + def close_tab(self, tab: Tab) -> None: if e := tab.editor: if e.content and e.content.editable and e.content.unsaved_changes: diff --git a/src/biscuit/layout/editors/tab.py b/src/biscuit/layout/editors/tab.py index 1dfe34e9..cf302e4c 100644 --- a/src/biscuit/layout/editors/tab.py +++ b/src/biscuit/layout/editors/tab.py @@ -63,9 +63,16 @@ def __init__(self, master: EditorPane, editor: Editor, *args, **kwargs) -> None: self.bind("", self.select) self.name.bind("", self.select) + self.bind("", self.on_drag) + self.name.bind("", self.on_drag) + self.bind("", self.on_hover) self.bind("", self.off_hover) + def on_drag(self, event: tk.Event) -> None: + if hasattr(self.master, "drag_tab"): + self.master.drag_tab(self, event.x_root) + def close(self, *_) -> None: self.master.close_tab(self) diff --git a/src/biscuit/layout/editors/tabscroll.py b/src/biscuit/layout/editors/tabscroll.py index 0d89912e..a2ed7414 100644 --- a/src/biscuit/layout/editors/tabscroll.py +++ b/src/biscuit/layout/editors/tabscroll.py @@ -48,6 +48,18 @@ def __init__(self, master: EditorsBar, *args, **kwargs) -> None: self.left.pack(side=tk.LEFT, fill=tk.BOTH) self.right.pack(side=tk.RIGHT, fill=tk.BOTH, padx=(0, 1)) - # TODO: Implement scroll_left and scroll_right methods - def scroll_left(self) -> None: ... - def scroll_right(self) -> None: ... + def scroll_left(self) -> None: + if hasattr(self.master, "tabs") and hasattr(self.master.tabs, "xview_scroll"): + self.master.tabs.xview_scroll(1, "units") + elif hasattr(self.master, "active_pane") and self.master.active_pane: + self.master.active_pane.scroll_tabs_left() + elif hasattr(self.master, "scroll_tabs"): + self.master.scroll_tabs(1) + + def scroll_right(self) -> None: + if hasattr(self.master, "tabs") and hasattr(self.master.tabs, "xview_scroll"): + self.master.tabs.xview_scroll(-1, "units") + elif hasattr(self.master, "active_pane") and self.master.active_pane: + self.master.active_pane.scroll_tabs_right() + elif hasattr(self.master, "scroll_tabs"): + self.master.scroll_tabs(-1) diff --git a/src/biscuit/layout/menubar/item.py b/src/biscuit/layout/menubar/item.py index 1689acd4..9b2c907a 100644 --- a/src/biscuit/layout/menubar/item.py +++ b/src/biscuit/layout/menubar/item.py @@ -10,13 +10,15 @@ class MenubarItem(Menubutton): - def __init__(self, master: Menubar, text, *args, **kwargs) -> None: + def __init__(self, master: Menubar, text, underline: int = -1, *args, **kwargs) -> None: super().__init__(master, *args, **kwargs) self.name = text + self.underline = underline self.config( text=text, padx=10, pady=5, + underline=underline, font=self.base.settings.uifont, **self.base.theme.layout.menubar.item, ) diff --git a/src/biscuit/layout/menubar/menubar.py b/src/biscuit/layout/menubar/menubar.py index 1446818c..7a9619a1 100644 --- a/src/biscuit/layout/menubar/menubar.py +++ b/src/biscuit/layout/menubar/menubar.py @@ -131,13 +131,14 @@ def moving(self, event: tk.Event) -> None: y = event.y_root - self.y self.base.geometry(f"+{x}+{y}") - def add_menu(self, text: str) -> Menu: + def add_menu(self, text: str, underline: int = -1) -> Menu: """Add a new menu to the menubar Args: - text (str): Text of the menu""" + text (str): Text of the menu + underline (int): Index of the mnemonic letter to underline""" - new_menu = MenubarItem(self, text) + new_menu = MenubarItem(self, text, underline=underline) new_menu.pack(side=tk.LEFT, fill=tk.BOTH, in_=self.container) self.menus.append(new_menu) @@ -148,11 +149,28 @@ def add_menus(self) -> None: self.add_edit_menu() self.add_view_menu() self.add_help_menu() + self.bind_accelerators() + + def bind_accelerators(self) -> None: + menu_keys = {"f": "File", "e": "Edit", "v": "View", "h": "Help"} + for key, name in menu_keys.items(): + for item in self.menus: + if item.name.lower() == name.lower(): + try: + self.base.bind(f"", lambda e, it=item: self._activate_menu(it)) + self.base.bind(f"", lambda e, it=item: self._activate_menu(it)) + except Exception: + pass + + def _activate_menu(self, item: MenubarItem) -> str: + self.close_all_menus() + item.onclick() + return "break" def add_file_menu(self) -> None: events = self.events - self.file_menu = self.add_menu("File") + self.file_menu = self.add_menu("File", underline=0) self.file_menu.add_command("New File", events.new_file) self.file_menu.add_command("New Window", events.new_window) self.file_menu.add_separator() @@ -183,7 +201,7 @@ def add_file_menu(self) -> None: def add_edit_menu(self) -> None: events = self.events - self.edit_menu = self.add_menu("Edit") + self.edit_menu = self.add_menu("Edit", underline=0) self.edit_menu.add_command("Undo", events.undo) self.edit_menu.add_command("Redo", events.redo) self.edit_menu.add_separator() @@ -206,7 +224,7 @@ def add_edit_menu(self) -> None: def add_view_menu(self) -> None: events = self.events - self.view_menu = self.add_menu("View") + self.view_menu = self.add_menu("View", underline=0) # TODO: Add the rest of the view menu items self.view_menu.add_command("Command Palette...", events.show_command_palette) self.view_menu.add_command("Explorer", events.show_directory_tree) @@ -221,7 +239,7 @@ def add_view_menu(self) -> None: def add_help_menu(self) -> None: events = self.events - self.help_menu = self.add_menu("Help") + self.help_menu = self.add_menu("Help", underline=0) self.help_menu.add_command("Welcome", events.show_welcome) self.help_menu.add_command("Show All Commands", events.show_command_palette) self.help_menu.add_command("Documentation", events.open_biscuit_documentation) diff --git a/src/biscuit/layout/panel/panel.py b/src/biscuit/layout/panel/panel.py index b4e86762..bcfaac9c 100644 --- a/src/biscuit/layout/panel/panel.py +++ b/src/biscuit/layout/panel/panel.py @@ -32,7 +32,13 @@ def __init__(self, master: Content, *args, **kwargs) -> None: self.views: list[PanelView] = [] - self.default_views = [Problems(self), Logs(self), Inspect(self), Terminal(self)] + self.default_views = [ + Problems(self), + Logs(self), + Inspect(self), + Terminal(self), + GitGraph(self), + ] self.add_views(self.default_views) def add_views(self, views: list[PanelView]) -> None: @@ -108,6 +114,17 @@ def show_logs(self, *_) -> None: self.set_active_view(self.logger) self.show_panel() + @property + def gitgraph(self) -> GitGraph: + return self.default_views[4] + + def show_gitgraph(self, *_) -> None: + """Shows the git graph if its hidden/minimized.""" + + self.set_active_view(self.gitgraph) + self.gitgraph.refresh() + self.show_panel() + def toggle_panel(self, *_) -> None: """Toggles the panel visibility.""" diff --git a/src/biscuit/layout/statusbar/statusbar.py b/src/biscuit/layout/statusbar/statusbar.py index af49e684..ece9d70c 100644 --- a/src/biscuit/layout/statusbar/statusbar.py +++ b/src/biscuit/layout/statusbar/statusbar.py @@ -138,6 +138,33 @@ def __init__(self, master: Frame, *args, **kwargs) -> None: side=tk.RIGHT, ) + # --------------------------------------------------------------------- + self.zoom_actionset = self.create_actionset( + "Change editor font size", + "zoom:", + [ + ("8pt (Small)", lambda e=None: self.change_font_size(8)), + ("10pt (Normal)", lambda e=None: self.change_font_size(10)), + ("12pt (Default)", lambda e=None: self.change_font_size(12)), + ("14pt (Medium)", lambda e=None: self.change_font_size(14)), + ("16pt (Large)", lambda e=None: self.change_font_size(16)), + ("18pt (Extra Large)", lambda e=None: self.change_font_size(18)), + ("20pt (Huge)", lambda e=None: self.change_font_size(20)), + ], + pinned=[ + ["custom: {}pt", self.change_custom_font_size], + ], + ) + current_font_size = 12 + if hasattr(self.base, "config") and hasattr(self.base.config, "font"): + current_font_size = self.base.config.font[1] + self.font_size_btn = self.add_button( + text=f"{current_font_size}pt", + callback=self.base.commands.change_font_size, + description="Change editor font size", + side=tk.RIGHT, + ) + # --------------------------------------------------------------------- items = [ (aliases[0], self.change_language(aliases[0])) @@ -390,3 +417,30 @@ def change_language(self, language: str) -> typing.Callable: return lambda _: self.base.editorsmanager.active_editor.content.text.highlighter.change_language( language ) + + def change_font_size(self, size: int) -> None: + try: + size = int(size) + except (ValueError, TypeError): + return + + if hasattr(self.base, "settings") and hasattr(self.base.settings, "font"): + try: + self.base.settings.font.configure(size=size) + except Exception: + pass + + if hasattr(self.base, "editorsmanager"): + editor = self.base.editorsmanager.active_editor + if editor and hasattr(editor, "content") and hasattr(editor.content, "set_fontsize"): + try: + editor.content.set_fontsize(size) + except Exception: + pass + + self.font_size_btn.set_text(f"{size}pt") + + def change_custom_font_size(self, text: str = None) -> None: + if text and text.strip().isnumeric(): + self.change_font_size(int(text.strip())) + diff --git a/src/biscuit/session.py b/src/biscuit/session.py index 287dd024..e3836b9f 100644 --- a/src/biscuit/session.py +++ b/src/biscuit/session.py @@ -1,6 +1,10 @@ # Credits: RINO-GAELICO from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from biscuit import App import toml diff --git a/src/biscuit/settings/config.py b/src/biscuit/settings/config.py index 2322c199..92f5cfec 100644 --- a/src/biscuit/settings/config.py +++ b/src/biscuit/settings/config.py @@ -8,11 +8,14 @@ class Config: """Loads and manages configurations for biscuit.""" + registered_themes: dict = {} + def __init__(self, master) -> None: self.base = master.base self.config_path = self.get_config_path("settings.toml") self.data = {} + self.registered_themes = {} self.load_data() def get_config_path(self, relative_path: str) -> str: @@ -38,6 +41,34 @@ def load_data(self) -> None: self.setup_properties() + def register_theme(self, name: str, theme_cls_or_instance) -> None: + """Register a custom theme class or instance.""" + if "registered_themes" not in self.__dict__: + self.registered_themes = dict(self.registered_themes) + self.registered_themes[name.lower()] = theme_cls_or_instance + + def set_theme(self, theme_name: str) -> None: + """Set the active theme at runtime.""" + from .theme import VSCodeDark, VSCodeLight + builtin = { + "dark": VSCodeDark, + "light": VSCodeLight, + "gruvbox_dark": GruvboxDark, + "catppuccin_mocha": CatppuccinMocha, + } + theme_key = str(theme_name).lower() + if theme_key in self.registered_themes: + target = self.registered_themes[theme_key] + self.theme = target() if callable(target) else target + elif theme_key in builtin: + self.theme = builtin[theme_key]() + else: + self.theme = VSCodeDark() + + self.data["theme"] = theme_key + if hasattr(self.base, "theme"): + self.base.theme = self.theme + def setup_properties(self) -> None: """Setup properties based on the loaded data.""" # TODO add more properties @@ -47,17 +78,7 @@ def setup_properties(self) -> None: # Theme theme_name = self.get_value("theme", "dark") - from .theme import VSCodeDark, VSCodeLight - if theme_name == "dark": - self.theme = VSCodeDark() - elif theme_name == "light": - self.theme = VSCodeLight() - elif theme_name == "gruvbox_dark": - self.theme = GruvboxDark() - elif theme_name == "catppuccin_mocha": - self.theme = CatppuccinMocha() - else: - self.theme = VSCodeDark() + self.set_theme(theme_name) # Text Editor self.auto_save_enabled = self.get_value("auto_save", False) diff --git a/src/biscuit/settings/editor/editor.py b/src/biscuit/settings/editor/editor.py index da60f87c..7f54f731 100644 --- a/src/biscuit/settings/editor/editor.py +++ b/src/biscuit/settings/editor/editor.py @@ -101,11 +101,10 @@ def add_ai(self): ai = self.add_section("AI") _auto_register() - models = list_models() + current = self.base.config.get_nested("ai.default_model", "gemini-2.5-flash") + models = list_models(current) model_ids = list(models.values()) model_names = list(models.keys()) - - current = self.base.config.get_nested("ai.default_model", "gemini-2.5-flash") try: default_idx = model_ids.index(current) except ValueError: @@ -177,12 +176,16 @@ def show_result(self, term: str): Args: term (str): the search term""" - term = term.lower() + term = term.lower().strip() + total_visible = 0 for section in self.sections: visible_items = 0 for item in section.items: - if term in item.name.lower(): + desc = getattr(item, "description", None) + matches_name = term in item.name.lower() + matches_desc = desc is not None and term in str(desc).lower() + if not term or matches_name or matches_desc: item.pack(fill=tk.X, expand=True) visible_items += 1 else: @@ -190,9 +193,13 @@ def show_result(self, term: str): if visible_items > 0: section.pack(fill=tk.X, expand=True) + total_visible += visible_items else: section.pack_forget() + if total_visible == 0 and term: + self.show_no_results() + def show_no_results(self): """Show no results found message in the settings editor""" # TODO implement this diff --git a/src/biscuit/settings/editor/items.py b/src/biscuit/settings/editor/items.py index 2053106d..45577417 100644 --- a/src/biscuit/settings/editor/items.py +++ b/src/biscuit/settings/editor/items.py @@ -307,4 +307,93 @@ def cancel(): relief=tk.FLAT, font=self.base.settings.uifont).pack(side=tk.RIGHT, padx=2) -# TODO list item with add to list button for taking list values +class ListItem(Item): + """Dynamic list item widget for taking list of string values with add/remove controls.""" + + def __init__( + self, + master, + name="Example", + value=None, + callback=None, + description=None, + *args, + **kwargs, + ) -> None: + super().__init__(master, name, callback, *args, **kwargs) + self.description = description + self.value = list(value) if value else [] + + self.rows_frame = Frame(self, bg=self.bg) + self.rows_frame.pack(fill=tk.X, expand=True, pady=(5, 0)) + + add_frame = Frame(self, bg=self.bg) + add_frame.pack(fill=tk.X, expand=True, pady=(5, 0)) + + self.add_entry_var = tk.StringVar() + self.add_entry = tk.Entry( + add_frame, + textvariable=self.add_entry_var, + font=self.base.settings.uifont, + bg=self.highlightbg, + fg=self.fg, + relief=tk.FLAT, + ) + self.add_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) + self.add_entry.bind("", lambda _: self.add_item()) + + tk.Button( + add_frame, + text="Add", + command=self.add_item, + font=self.base.settings.uifont, + bg=self.highlightbg, + fg=self.fg, + relief=tk.FLAT, + cursor="hand2", + ).pack(side=tk.RIGHT) + + self.render_rows() + + def render_rows(self) -> None: + for child in self.rows_frame.winfo_children(): + child.destroy() + + for idx, item_val in enumerate(self.value): + row = Frame(self.rows_frame, bg=self.bg) + row.pack(fill=tk.X, expand=True, pady=2) + + tk.Label( + row, + text=str(item_val), + font=self.base.settings.uifont, + bg=self.bg, + fg=self.fg, + anchor=tk.W, + ).pack(side=tk.LEFT, fill=tk.X, expand=True) + + tk.Button( + row, + text="✕", + command=lambda i=idx: self.remove_item(i), + font=self.base.settings.uifont, + bg=self.bg, + fg=self.fg, + relief=tk.FLAT, + cursor="hand2", + ).pack(side=tk.RIGHT) + + def add_item(self, val: str = None) -> None: + new_val = val if val is not None else self.add_entry_var.get().strip() + if not new_val: + return + self.value.append(new_val) + self.add_entry_var.set("") + self.render_rows() + self.change() + + def remove_item(self, index: int) -> None: + if 0 <= index < len(self.value): + self.value.pop(index) + self.render_rows() + self.change() diff --git a/src/biscuit/settings/editor/search_filter.py b/src/biscuit/settings/editor/search_filter.py new file mode 100644 index 00000000..ba7defc6 --- /dev/null +++ b/src/biscuit/settings/editor/search_filter.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import Any, List, Optional + + +class SettingsSearchFilter: + """Helper for evaluating and matching settings items against a search query.""" + + @staticmethod + def matches(name: str, description: Optional[str], term: str) -> bool: + if not term: + return True + term = term.strip().lower() + if term in (name or "").lower(): + return True + if description and term in str(description).lower(): + return True + return False + + @staticmethod + def filter_items(items: List[Any], term: str) -> List[Any]: + term = term.strip().lower() + if not term: + return list(items) + return [ + item for item in items + if SettingsSearchFilter.matches( + getattr(item, "name", ""), + getattr(item, "description", None), + term + ) + ] diff --git a/src/biscuit/settings/editor/section.py b/src/biscuit/settings/editor/section.py index a6ceba73..a1b25e8d 100644 --- a/src/biscuit/settings/editor/section.py +++ b/src/biscuit/settings/editor/section.py @@ -2,7 +2,7 @@ from biscuit.common.ui import Frame -from .items import CheckboxItem, DropdownItem, IntegerItem, StringItem, PasswordItem, MCPServersItem +from .items import CheckboxItem, DropdownItem, IntegerItem, StringItem, PasswordItem, MCPServersItem, ListItem class Section(Frame): @@ -95,3 +95,9 @@ def add_checkbox(self, name="Example", default=True, callback=None) -> None: dropdown = CheckboxItem(self, name, default, callback) dropdown.pack(fill=tk.X, expand=True) self.items.append(dropdown) + + def add_list(self, name="Example", default=None, callback=None, description=None): + item = ListItem(self, name, default, callback, description) + item.pack(fill=tk.X, expand=True) + self.items.append(item) + return item diff --git a/src/biscuit/settings/font.py b/src/biscuit/settings/font.py new file mode 100644 index 00000000..519d1cfb --- /dev/null +++ b/src/biscuit/settings/font.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from typing import List, Optional + + +DEFAULT_FALLBACK_FONTS = [ + "Consolas", + "Courier New", + "DejaVu Sans Mono", + "Fira Code", + "JetBrains Mono", + "Lucida Console", + "Segoe UI", +] + + +class FontManager: + """Manages system and fallback font discovery and configuration.""" + + @staticmethod + def get_system_font_families() -> List[str]: + try: + import tkinter.font as tkfont + import tkinter as tk + # Only query if a root window exists to avoid headless crashes + root = tk._default_root + if root: + fonts = list(set(tkfont.families(root))) + fonts.sort() + return [f for f in fonts if not f.startswith("@")] + except Exception: + pass + return list(DEFAULT_FALLBACK_FONTS) + + @staticmethod + def is_font_available(font_family: str, available_fonts: Optional[List[str]] = None) -> bool: + fonts = available_fonts or FontManager.get_system_font_families() + return font_family.lower() in [f.lower() for f in fonts] diff --git a/src/biscuit/settings/settings.py b/src/biscuit/settings/settings.py index ef5b0960..baf5bd75 100644 --- a/src/biscuit/settings/settings.py +++ b/src/biscuit/settings/settings.py @@ -5,7 +5,10 @@ import tkinter.font as tkfont import typing -import tkextrafont as extra +try: + import tkextrafont as extra +except Exception: + extra = None from ..common import ActionSet, extract_commands, formalize_command from .bindings import Bindings @@ -84,12 +87,14 @@ def setup_icon(self) -> None: pass def setup_font(self) -> None: - try: - self.iconfont = extra.Font( - file=self.resources.get_font("codicon.ttf"), family="codicon" - ) - except tk.TclError: - pass + self.iconfont = None + if extra is not None: + try: + self.iconfont = extra.Font( + file=self.resources.get_font("codicon.ttf"), family="codicon" + ) + except Exception: + self.iconfont = None self.font = tkfont.Font(family=self.config.font[0], size=self.config.font[1]) self.font_bold = tkfont.Font( diff --git a/src/biscuit/settings/theme/__init__.py b/src/biscuit/settings/theme/__init__.py index 63f477c5..26dc057a 100644 --- a/src/biscuit/settings/theme/__init__.py +++ b/src/biscuit/settings/theme/__init__.py @@ -1,3 +1,4 @@ from .theme import Theme from .vscdark import VSCodeDark from .vsclight import VSCodeLight +from .high_contrast import HighContrastDark, HighContrastLight diff --git a/src/biscuit/settings/theme/high_contrast.py b/src/biscuit/settings/theme/high_contrast.py new file mode 100644 index 00000000..445b0f0a --- /dev/null +++ b/src/biscuit/settings/theme/high_contrast.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import sv_ttk +from .theme import Theme + + +class HighContrastDark(Theme): + name = "High Contrast Dark" + + border = "#6FC3DF" + disabled = "#7F7F7F" + + primary_background = "#000000" + primary_foreground = "#FFFFFF" + primary_background_highlight = "#1F1F1F" + primary_foreground_highlight = "#FFFFFF" + + secondary_background = "#0A0A0A" + secondary_foreground = "#FFFFFF" + secondary_background_highlight = "#2A2A2A" + secondary_foreground_highlight = "#FFFFFF" + + # High-contrast syntax tokens (WCAG AAA compliant) + Keyword = "#569CD6" + Name = "#4EC9B0" + Function = "#DCDCAA" + String = "#CE9178" + Number = "#B5CEA8" + Comment = {"foreground": "#7CA668", "font": {"slant": "italic"}} + Punctuation = "#FFFFFF" + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + try: + sv_ttk.use_dark_theme() + except Exception: + pass + + self.editors.linenumbers.number.foreground = "#FFFFFF" + self.editors.linenumbers.number.highlightforeground = "#FFFF00" + self.editors.linenumbers.breakpoint.background = "#FF0000" + self.editors.linenumbers.breakpoint.highlightbackground = "#FF0000" + + +class HighContrastLight(Theme): + name = "High Contrast Light" + + border = "#0F4A85" + disabled = "#666666" + + primary_background = "#FFFFFF" + primary_foreground = "#000000" + primary_background_highlight = "#E5E5E5" + primary_foreground_highlight = "#000000" + + secondary_background = "#F0F0F0" + secondary_foreground = "#000000" + secondary_background_highlight = "#DCDCDC" + secondary_foreground_highlight = "#000000" + + # High-contrast light syntax tokens + Keyword = "#0000FF" + Name = "#267F99" + Function = "#795E26" + String = "#A31515" + Number = "#098658" + Comment = {"foreground": "#008000", "font": {"slant": "italic"}} + Punctuation = "#000000" + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + try: + sv_ttk.use_light_theme() + except Exception: + pass + + self.editors.linenumbers.number.foreground = "#000000" + self.editors.linenumbers.number.highlightforeground = "#0000FF" + self.editors.linenumbers.breakpoint.background = "#B00000" + self.editors.linenumbers.breakpoint.highlightbackground = "#B00000" diff --git a/src/biscuit/views/__init__.py b/src/biscuit/views/__init__.py index c75e0f36..282a4683 100644 --- a/src/biscuit/views/__init__.py +++ b/src/biscuit/views/__init__.py @@ -4,6 +4,7 @@ from .explorer import Explorer from .extensions import Extensions from .github import GitHub +from .gitgraph import GitGraph from .logs import Logs from .outline import Outline from .panelview import PanelView diff --git a/src/biscuit/views/ai/ai.py b/src/biscuit/views/ai/ai.py index e31e8350..a6070ee4 100644 --- a/src/biscuit/views/ai/ai.py +++ b/src/biscuit/views/ai/ai.py @@ -28,7 +28,9 @@ def __init__(self, master, *args, **kwargs) -> None: self.title.grid_forget() _auto_register() - self.available_models = list_models() + self.available_models = list_models( + self.base.config.get_nested("ai.default_model", "") + ) self.current_model = self._load_default_model() self.top.grid_columnconfigure(self.column, weight=1) @@ -36,6 +38,7 @@ def __init__(self, master, *args, **kwargs) -> None: self.menu = self._build_menu() self.add_action(Icons.REFRESH, self.new_chat) + self.add_action(Icons.DEBUG_STOP, self.stop_chat) self.add_action(Icons.COPY, self.copy_chat) self.add_action(Icons.ELLIPSIS, self.menu.show) self.add_action(Icons.SETTINGS, self.open_settings) @@ -43,14 +46,12 @@ def __init__(self, master, *args, **kwargs) -> None: self.add_chat() def _load_default_model(self) -> str: - models = list_models() - ids = list(models.values()) saved = self.base.config.get_nested("ai.default_model", "") - if saved in ids: - for name, mid in models.items(): - if mid == saved: - return name - return ids[0] if ids else "" + models = list_models(saved) + for name, mid in models.items(): + if mid == saved: + return name + return next(iter(models), "") def _has_any_key(self) -> bool: for prov in ("gemini", "anthropic", "groq", "minimax"): @@ -174,4 +175,7 @@ def show_stats(self) -> None: msg += f" | Tokens: {self.agent.input_tokens + self.agent.output_tokens}" self.base.notifications.info(msg) except Exception as e: - print(f"Error showing stats: {e}") \ No newline at end of file + print(f"Error showing stats: {e}") + def stop_chat(self) -> None: + if self.agent: + self.agent.stop_execution() diff --git a/src/biscuit/views/debug/debug.py b/src/biscuit/views/debug/debug.py index e430375b..5868ae2c 100644 --- a/src/biscuit/views/debug/debug.py +++ b/src/biscuit/views/debug/debug.py @@ -75,8 +75,8 @@ def run_config(self, *_): try: if self.selected_config: config = self.configs[self.selected_config] - language = config["language"] - if debugger := self.manager.request_debugger(config["language"]): + language = config.get("language", "dap") + if debugger := self.manager.request_debugger(language): debugger.launch_config(config) else: self.base.notifications.error( diff --git a/src/biscuit/views/debug/watch.py b/src/biscuit/views/debug/watch.py new file mode 100644 index 00000000..314d7d42 --- /dev/null +++ b/src/biscuit/views/debug/watch.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import tkinter as tk +from typing import Any, Dict, List, Optional + +from biscuit.common.ui import Tree +from ..sidebar_item import SideBarViewItem + + +class WatchManager: + """Manages and evaluates watch expressions across debugger frames.""" + + def __init__(self): + self.expressions: List[str] = [] + + def add_expression(self, expr: str) -> None: + expr = expr.strip() + if expr and expr not in self.expressions: + self.expressions.append(expr) + + def remove_expression(self, expr: str) -> bool: + if expr in self.expressions: + self.expressions.remove(expr) + return True + return False + + def clear(self) -> None: + self.expressions.clear() + + def evaluate_all(self, scope: Optional[Dict[str, Any]] = None) -> Dict[str, str]: + results = {} + eval_scope = scope if scope is not None else {} + for expr in self.expressions: + try: + val = eval(expr, {"__builtins__": {}}, eval_scope) + results[expr] = repr(val) + except Exception as e: + results[expr] = f"" + return results + + +class Watch(SideBarViewItem): + """Debug sidebar pane for watch expressions.""" + + def __init__(self, master, *args, **kwargs) -> None: + self.title = "Watch" + self.__actions__ = () + super().__init__(master, itembar=True, *args, **kwargs) + + self.manager = WatchManager() + self.tree = Tree( + self.content, + style="mono.Treeview", + columns=("expression", "value"), + *args, + **kwargs, + ) + self.tree.grid(row=0, column=0, sticky=tk.NSEW) + + def update_watches(self, scope: Optional[Dict[str, Any]] = None) -> None: + results = self.manager.evaluate_all(scope) + self.tree.clear() + for expr, val in results.items(): + self.tree.insert("", "end", text=expr, values=(val,)) diff --git a/src/biscuit/views/explorer/directorytree.py b/src/biscuit/views/explorer/directorytree.py index 0ab3bc06..7d8f4b1c 100644 --- a/src/biscuit/views/explorer/directorytree.py +++ b/src/biscuit/views/explorer/directorytree.py @@ -19,6 +19,37 @@ from .watcher import DirectoryTreeWatcher +def get_file_icon(filename: str) -> str: + """Return matching icon glyph for a given filename based on extension.""" + name = filename.lower() + ext = os.path.splitext(name)[1] + + if ext in ( + ".py", ".pyw", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", + ".c", ".h", ".cpp", ".hpp", ".cc", ".cxx", ".rs", ".go", ".java", + ".rb", ".php", ".cs", ".lua", ".html", ".htm", ".css", ".scss", + ".sass", ".less", ".sql", ".sh", ".bash", ".zsh", ".toml", ".yaml", + ".yml", ".swift", ".kt", ".kts", ".scala", ".zig", ".dart", + ): + return Icons.FILE_CODE + elif ext in (".json",): + return Icons.JSON + elif ext in (".md", ".markdown", ".txt", ".rst"): + return Icons.MARKDOWN + elif ext in ( + ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", ".webp", + ".mp4", ".mp3", ".wav", ".ogg", ".webm", ".flac", + ): + return Icons.FILE_MEDIA + elif ext in (".pdf",): + return Icons.FILE_PDF + elif ext in (".zip", ".tar", ".gz", ".bz2", ".7z", ".rar", ".xz"): + return Icons.FILE_ZIP + elif ext in (".exe", ".dll", ".so", ".dylib", ".bin", ".o", ".a"): + return Icons.FILE_BINARY + return Icons.FILE + + class DirectoryTree(SideBarViewItem): """A view that displays the directory tree. @@ -232,9 +263,8 @@ def update_treeview(self, parent_path: str, parent="") -> None: node = self.tree.insert( parent or "", "end", - text=f" {name}", + text=f" {Icons.FOLDER} {name}", values=[path, "directory"], - # image="foldericon", open=False, tags="ignored" if ignored and (unixlike in ignored) else "", ) @@ -251,13 +281,12 @@ def update_treeview(self, parent_path: str, parent="") -> None: continue try: - # TODO check filetype and get matching icon, cases + icon = get_file_icon(name) node = self.tree.insert( parent, "end", - text=f" {name}", + text=f" {icon} {name}", values=[path, "file"], - # image="document", tags="ignored" if ignored and (unixlike in ignored) else "", ) self.nodes[os.path.abspath(path)] = node @@ -471,5 +500,8 @@ def openfile(self, _) -> None: self.base.open_editor(path) def preview_file(self, _) -> None: - # TODO preview editors -- extra preview param for editors - return + if self.tree.selected_type() != "file": + return + path = self.tree.selected_path() + if hasattr(self.base, "open_editor"): + self.base.open_editor(path, preview=True) diff --git a/src/biscuit/views/explorer/dnd.py b/src/biscuit/views/explorer/dnd.py new file mode 100644 index 00000000..ff3d12b7 --- /dev/null +++ b/src/biscuit/views/explorer/dnd.py @@ -0,0 +1,25 @@ +from __future__ import annotations +import os +import shutil +from typing import Optional + +class FileDragDropController: + def __init__(self): + self.dragged_path: Optional[str] = None + + def start_drag(self, source_path: str): + self.dragged_path = source_path + + def drop_on(self, target_directory: str, copy: bool = False) -> str: + if not self.dragged_path or not os.path.exists(self.dragged_path): + raise FileNotFoundError("Source path does not exist") + dest = os.path.join(target_directory, os.path.basename(self.dragged_path)) + if copy: + if os.path.isdir(self.dragged_path): + shutil.copytree(self.dragged_path, dest) + else: + shutil.copy2(self.dragged_path, dest) + else: + shutil.move(self.dragged_path, dest) + self.dragged_path = None + return dest diff --git a/src/biscuit/views/explorer/multi_root.py b/src/biscuit/views/explorer/multi_root.py new file mode 100644 index 00000000..5a7cadb8 --- /dev/null +++ b/src/biscuit/views/explorer/multi_root.py @@ -0,0 +1,27 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import List, Optional + +@dataclass +class WorkspaceFolder: + name: str + path: str + +class MultiRootWorkspaceManager: + def __init__(self): + self.folders: List[WorkspaceFolder] = [] + + def add_folder(self, name: str, path: str) -> WorkspaceFolder: + folder = WorkspaceFolder(name, path) + if folder not in self.folders: + self.folders.append(folder) + return folder + + def remove_folder(self, path: str) -> Optional[WorkspaceFolder]: + for i, f in enumerate(self.folders): + if f.path == path: + return self.folders.pop(i) + return None + + def list_folders(self) -> List[WorkspaceFolder]: + return list(self.folders) diff --git a/src/biscuit/views/explorer/nesting.py b/src/biscuit/views/explorer/nesting.py new file mode 100644 index 00000000..ce42a6bc --- /dev/null +++ b/src/biscuit/views/explorer/nesting.py @@ -0,0 +1,26 @@ +from __future__ import annotations +import os +from typing import Dict, List, Optional + +class FileNestingController: + def __init__(self, patterns: Dict[str, str] = None): + self.patterns = patterns or { + "*.ts": "${capture}.js, ${capture}.d.ts", + "*.py": "${capture}.pyc", + "package.json": "package-lock.json", + } + + def should_nest_under(self, child_file: str, parent_file: str) -> bool: + child_base = os.path.basename(child_file) + parent_base = os.path.basename(parent_file) + + if parent_base == "package.json" and child_base == "package-lock.json": + return True + + if parent_base.endswith(".ts") and (child_base == parent_base[:-3] + ".js" or child_base == parent_base[:-3] + ".d.ts"): + return True + + if parent_base.endswith(".py") and child_base == parent_base + "c": + return True + + return False diff --git a/src/biscuit/views/extensions/__init__.py b/src/biscuit/views/extensions/__init__.py index 8530ea58..61712beb 100644 --- a/src/biscuit/views/extensions/__init__.py +++ b/src/biscuit/views/extensions/__init__.py @@ -1 +1,3 @@ from .extensions import Extensions +from .installed import Installed +from .results import Results diff --git a/src/biscuit/views/extensions/category_filter.py b/src/biscuit/views/extensions/category_filter.py new file mode 100644 index 00000000..2061da18 --- /dev/null +++ b/src/biscuit/views/extensions/category_filter.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Optional + + +class ExtensionCategory: + ALL = "All" + THEMES = "Themes" + LANGUAGE_SERVERS = "Language Servers" + FORMATTERS = "Formatters" + AI = "AI" + DEBUGGERS = "Debuggers" + + CATEGORIES = [ALL, THEMES, LANGUAGE_SERVERS, FORMATTERS, AI, DEBUGGERS] + + +class ExtensionCategoryFilter: + """Filters extension items by category or keyword tags.""" + + def __init__(self, active_category: str = ExtensionCategory.ALL): + self.active_category = active_category + + def matches(self, ext_data: Dict[str, Any], category: Optional[str] = None) -> bool: + cat = category or self.active_category + if cat == ExtensionCategory.ALL or not cat: + return True + + target = cat.lower() + tags = [str(t).lower() for t in ext_data.get("tags", [])] + category_field = str(ext_data.get("category", "")).lower() + description = str(ext_data.get("description", "")).lower() + + if target == "themes": + return "theme" in tags or "theme" in category_field or "theme" in description + elif target == "language servers": + return "lsp" in tags or "language-server" in tags or "lsp" in description or "language server" in category_field + elif target == "formatters": + return "formatter" in tags or "formatting" in tags or "formatter" in category_field + elif target == "ai": + return "ai" in tags or "llm" in tags or "assistant" in tags or "ai" in category_field + elif target == "debuggers": + return "debugger" in tags or "dap" in tags or "debug" in tags or "debugger" in category_field + + return target in category_field or target in tags + + def filter_list(self, extensions: List[Dict[str, Any]], category: Optional[str] = None) -> List[Dict[str, Any]]: + return [ext for ext in extensions if self.matches(ext, category)] diff --git a/src/biscuit/views/extensions/extension.py b/src/biscuit/views/extensions/extension.py index b8ecb692..17e6c4e1 100644 --- a/src/biscuit/views/extensions/extension.py +++ b/src/biscuit/views/extensions/extension.py @@ -56,19 +56,22 @@ def __init__( ) # TODO: don't do this here, do it when install is called - self.submodule_repo = Submodule( - s.repo, - s.binsha, - s.mode, - s.path, - s.name, - s.parent_commit, - s.url, - # hack to get gitpython working with our setup - # since gitpython is stupid and doesn't let us modify the default target branch easily - # i.e it has default=`master` and we use `main` - "refs/heads/main", - ) + try: + self.submodule_repo = Submodule( + s.repo, + s.binsha, + s.mode, + s.path, + s.name, + s.parent_commit, + s.url, + # hack to get gitpython working with our setup + # since gitpython is stupid and doesn't let us modify the default target branch easily + # i.e it has default=`master` and we use `main` + "refs/heads/main", + ) + except Exception: + self.submodule_repo = s self.path = Path(self.base.extensiondir) / "extensions" / self.id # GUI ---------------- @@ -118,9 +121,12 @@ def __init__( self.install = HoverChangeButton(self.subcontainer, "Install", padx=10) self.install.config(font=("Segoi UI", 8), pady=2) self.install.pack(side=tk.RIGHT, fill=tk.X) - + if self.installed: - self.set_installed() + if self.outdated: + self.set_outdated() + else: + self.set_installed() else: self.set_uninstalled() @@ -135,6 +141,32 @@ def __init__( self.bind("", self.hoveroff) self.hoveroff() + @property + def installed_version(self) -> str | None: + if self.id in self.manager.installed: + inst = self.manager.installed[self.id] + if isinstance(inst, dict): + return inst.get("version") + return None + + @property + def outdated(self) -> bool: + if not self.installed: + return False + inst_ver = self.installed_version + if not inst_ver or not self.version: + return False + return self._compare_versions(self.version, inst_ver) > 0 + + @staticmethod + def _compare_versions(v1: str, v2: str) -> int: + try: + t1 = tuple(int(p) for p in str(v1).strip().lstrip("v").split(".") if p.isdigit()) + t2 = tuple(int(p) for p in str(v2).strip().lstrip("v").split(".") if p.isdigit()) + return (t1 > t2) - (t1 < t2) + except Exception: + return 0 if v1 == v2 else 1 + @property def installed(self): return self.submodule_repo and self.submodule_repo.module_exists() and ( @@ -147,35 +179,64 @@ def uninstall_extension(self, *_): def install_extension(self, *_): self.manager.install_extension(self) - def set_unavailable(self): - self.install.text = "Error" - self.install.hovertext = "Retry" + def update_extension(self, *_): + self.manager.update_extension(self) + + def set_outdated(self): + self.install.set_command(self.update_extension) + self.install.text = "Update" + self.install.hovertext = f"v{self.version}" self.install.config( - text="Error", bg=self.base.theme.border, activebackground=self.base.theme.biscuit + text="Update", + bg="#2e7d32", + activebackground="#1b5e20", ) - def set_installed(self): - self.install.set_command(self.uninstall_extension) + def set_updating(self): + try: + self.install.config(text="Updating...", bg=self.base.theme.biscuit_dark) + except Exception: + pass - self.install.text = "Installed" - self.install.hovertext = "Uninstall" - self.install.config( - text="Installed", - bg=self.base.theme.border, - activebackground="#c61c1c", - ) + def set_unavailable(self): + try: + self.install.text = "Error" + self.install.hovertext = "Retry" + self.install.config( + text="Error", bg=self.base.theme.border, activebackground=self.base.theme.biscuit + ) + except Exception: + pass - def set_uninstalled(self): - self.install.set_command(self.install_extension) + def set_installed(self): + try: + self.install.set_command(self.uninstall_extension) + self.install.text = "Installed" + self.install.hovertext = "Uninstall" + self.install.config( + text="Installed", + bg=self.base.theme.border, + activebackground="#c61c1c", + ) + except Exception: + pass - self.install.config( - text="Install", bg=self.base.theme.biscuit, activebackground=self.base.theme.biscuit - ) - self.install.text = "Install" - self.install.hovertext = None + def set_uninstalled(self): + try: + self.install.set_command(self.install_extension) + self.install.config( + text="Install", bg=self.base.theme.biscuit, activebackground=self.base.theme.biscuit + ) + self.install.text = "Install" + self.install.hovertext = None + except Exception: + pass def set_fetching(self): - self.install.config(text="Fetching...", bg=self.base.theme.biscuit_dark) + try: + self.install.config(text="Fetching...", bg=self.base.theme.biscuit_dark) + except Exception: + pass def set_selected(self, *_): self.master.set_selected(self) diff --git a/src/biscuit/views/extensions/extensions.py b/src/biscuit/views/extensions/extensions.py index 38800e5e..350c8e52 100644 --- a/src/biscuit/views/extensions/extensions.py +++ b/src/biscuit/views/extensions/extensions.py @@ -4,6 +4,7 @@ from biscuit.common.ui import Entry from ..sidebar_view import SideBarView +from .installed import Installed from .results import Results @@ -24,6 +25,8 @@ def __init__(self, master, *args, **kwargs) -> None: self.searchbox = Entry(self, hint="Search extensions") self.searchbox.pack(fill=tk.X, anchor=tk.N, padx=10, pady=7) + self.installed = Installed(self) + self.add_item(self.installed) self.results = Results(self) self.add_item(self.results) self.add_action(Icons.FILTER, self.results.toggle_installed) diff --git a/src/biscuit/views/extensions/installed.py b/src/biscuit/views/extensions/installed.py new file mode 100644 index 00000000..416f935b --- /dev/null +++ b/src/biscuit/views/extensions/installed.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import tkinter as tk +import typing + +from ..sidebar_item import SideBarViewItem +from .placeholder import ExtensionsPlaceholder +from .results import ExtensionsList + +if typing.TYPE_CHECKING: + from .extension import ExtensionGUI + + +class Installed(SideBarViewItem): + """View that displays installed extensions separately from marketplace results.""" + + def __init__(self, master, *args, **kwargs) -> None: + self.__actions__ = () + self.title = "Installed" + + super().__init__(master, *args, **kwargs) + self.config(**self.base.theme.views.sidebar.item) + + self.placeholder = ExtensionsPlaceholder(self) + self.extension_list = ExtensionsList(self.content) + self.extension_list.pack(fill=tk.BOTH, expand=True) + + def add_extension(self, extension: ExtensionGUI) -> None: + self.extension_list.add(extension, fill=tk.X) + + def clear(self) -> None: + for widget in self.extension_list.items: + widget.pack_forget() + widget.destroy() + self.extension_list.items = [] diff --git a/src/biscuit/views/extensions/results.py b/src/biscuit/views/extensions/results.py index 97797028..ca42824a 100644 --- a/src/biscuit/views/extensions/results.py +++ b/src/biscuit/views/extensions/results.py @@ -79,7 +79,11 @@ def gui_refresh_loop(self) -> None: if not self.fetch_queue.empty(): name, data = self.fetch_queue.get() ext = ExtensionGUI(self, name, data) - if not self.filter_installed or (self.filter_installed and ext.installed): + if ext.installed and hasattr(self.master, "installed"): + ext.destroy() + inst_ext = ExtensionGUI(self.master.installed, name, data) + self.master.installed.add_extension(inst_ext) + elif not self.filter_installed: self.extension_list.add(ext, fill=tk.X) self.after(5, self.gui_refresh_loop) @@ -91,6 +95,8 @@ def clear(self, *_) -> None: self.content.update_idletasks() self.extension_list.items = [] + if hasattr(self.master, "installed"): + self.master.installed.clear() self.fetching.set() def set_selected(self, extension: ExtensionGUI) -> None: @@ -112,4 +118,9 @@ def search(self) -> None: self.clear() self.update_idletasks() - self.manager.display_filtered_extensions(self.master.searchbox.get()) + query = self.master.searchbox.get().strip() if hasattr(self.master, "searchbox") else "" + if not query: + self.manager.display_recommended_extensions() + else: + self.manager.display_filtered_extensions(query) + diff --git a/src/biscuit/views/gitgraph.py b/src/biscuit/views/gitgraph.py new file mode 100644 index 00000000..013c4809 --- /dev/null +++ b/src/biscuit/views/gitgraph.py @@ -0,0 +1,231 @@ +from __future__ import annotations + +import tkinter as tk +import tkinter.ttk as ttk +import typing + +from biscuit.common.icons import Icons +from biscuit.common.ui.hintedentry import Entry +from biscuit.common.ui.native import Frame, Label +from biscuit.common.ui.scrollbar import Scrollbar + +from .panelview import PanelView + +if typing.TYPE_CHECKING: + from biscuit.layout.panel import Panel + + +class GitGraph(PanelView): + """The Git Graph panel view. + + Displays visual branch graphs, commit history, authors, dates, branches/tags, + diff stats, and commit search. + """ + + def __init__(self, master: Panel, *args, **kwargs) -> None: + super().__init__(master, *args, **kwargs) + self.name = "Git Graph" + self.__icon__ = Icons.GIT_BRANCH + self.__actions__ = [ + (Icons.REFRESH, self.refresh), + (Icons.SEARCH, self.focus_search), + ] + + self.commits: list[dict[str, str]] = [] + + self.rowconfigure(1, weight=3) + self.rowconfigure(2, weight=1) + self.columnconfigure(0, weight=1) + + # 1. Search bar header + self.header = Frame(self, **self.base.theme.views.panel.logs) + self.header.grid(row=0, column=0, columnspan=2, sticky=tk.EW, padx=5, pady=4) + self.header.columnconfigure(0, weight=1) + + self.search_entry = Entry(self.header, hint="Search commits by message, author, hash, or branch...") + self.search_entry.grid(row=0, column=0, sticky=tk.EW, padx=(0, 10)) + self.search_entry.bind("", self.filter_commits) + + self.count_label = Label(self.header, text="0 commits", **self.base.theme.views.panel.logs) + self.count_label.grid(row=0, column=1, sticky=tk.E, padx=5) + + # 2. Main Commit Treeview + tree_frame = Frame(self) + tree_frame.grid(row=1, column=0, sticky=tk.NSEW) + tree_frame.rowconfigure(0, weight=1) + tree_frame.columnconfigure(0, weight=1) + + columns = ("graph", "hash", "refs", "message", "author", "date") + self.tree = ttk.Treeview( + tree_frame, + show="headings", + columns=columns, + selectmode=tk.BROWSE, + ) + self.tree.grid(row=0, column=0, sticky=tk.NSEW) + + self.tree.heading("graph", text="Graph", anchor=tk.W) + self.tree.heading("hash", text="Commit", anchor=tk.W) + self.tree.heading("refs", text="Refs", anchor=tk.W) + self.tree.heading("message", text="Message", anchor=tk.W) + self.tree.heading("author", text="Author", anchor=tk.W) + self.tree.heading("date", text="Date", anchor=tk.W) + + self.tree.column("graph", width=90, minwidth=60, stretch=False) + self.tree.column("hash", width=80, minwidth=70, stretch=False) + self.tree.column("refs", width=160, minwidth=100, stretch=False) + self.tree.column("message", width=320, minwidth=150, stretch=True) + self.tree.column("author", width=120, minwidth=80, stretch=False) + self.tree.column("date", width=100, minwidth=80, stretch=False) + + self.scrollbar = Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview) + self.scrollbar.grid(row=0, column=1, sticky=tk.NS) + self.tree.config(yscrollcommand=self.scrollbar.set) + + self.tree.bind("<>", self.on_select) + + # 3. Details pane + self.details_frame = Frame(self, **self.base.theme.views.panel.logs) + self.details_frame.grid(row=2, column=0, columnspan=2, sticky=tk.NSEW, padx=5, pady=(2, 5)) + self.details_frame.rowconfigure(0, weight=1) + self.details_frame.columnconfigure(0, weight=1) + + self.details_text = tk.Text( + self.details_frame, + height=4, + wrap=tk.WORD, + relief=tk.FLAT, + padx=8, + pady=4, + font=("Consolas", 10), + **self.base.theme.editors.text, + ) + self.details_text.grid(row=0, column=0, sticky=tk.NSEW) + self.details_scrollbar = Scrollbar(self.details_frame, orient=tk.VERTICAL, command=self.details_text.yview) + self.details_scrollbar.grid(row=0, column=1, sticky=tk.NS) + self.details_text.config(yscrollcommand=self.details_scrollbar.set) + self.details_text.insert(tk.END, "Select a commit to view details and changes.") + self.details_text.config(state=tk.DISABLED) + + # Listen for directory / repo changes + self.base.bind("<>", self.refresh, add=True) + + def focus_search(self, *_) -> None: + self.search_entry.entry.focus_set() + + def get_commit_graph_data(self) -> list[dict[str, str]]: + if not hasattr(self.base, "git") or not getattr(self.base.git, "repo", None): + return [] + + try: + repo = self.base.git.repo + out = repo.git.log( + "--graph", + "--all", + "--date=short", + "--format=%h%x00%d%x00%an%x00%ad%x00%s", + "-n", + "300", + ) + items: list[dict[str, str]] = [] + for line in out.splitlines(): + parts = line.split("\x00") + if len(parts) >= 5: + first = parts[0] + tokens = first.rsplit(" ", 1) + if len(tokens) == 2: + graph_symbol, commit_hash = tokens[0], tokens[1] + else: + graph_symbol, commit_hash = "", tokens[0] + + refs = parts[1].strip(" ()") + author = parts[2] + date = parts[3] + message = parts[4] + items.append( + { + "graph": graph_symbol, + "hash": commit_hash, + "refs": refs, + "message": message, + "author": author, + "date": date, + } + ) + elif line.strip(): + items.append( + { + "graph": line, + "hash": "", + "refs": "", + "message": "", + "author": "", + "date": "", + } + ) + return items + except Exception: + return [] + + def refresh(self, *_) -> None: + self.commits = self.get_commit_graph_data() + self.filter_commits() + + def filter_commits(self, *_) -> None: + raw = self.search_entry.get() + query = raw.strip().lower() if isinstance(raw, str) else "" + self.tree.delete(*self.tree.get_children()) + count = 0 + + for item in self.commits: + if query: + target = f"{item['hash']} {item['message']} {item['author']} {item['refs']}".lower() + if query not in target: + continue + + self.tree.insert( + "", + tk.END, + values=( + item["graph"], + item["hash"], + item["refs"], + item["message"], + item["author"], + item["date"], + ), + ) + if item["hash"]: + count += 1 + + self.count_label.config(text=f"{count} commits" if count != 1 else "1 commit") + + def on_select(self, *_) -> None: + selected = self.tree.selection() + if not selected: + return + values = self.tree.item(selected[0], "values") + if not values or len(values) < 6: + return + + commit_hash = values[1] + if not commit_hash: + return + + self.details_text.config(state=tk.NORMAL) + self.details_text.delete("1.0", tk.END) + + refs_str = f" ({values[2]})" if values[2] else "" + header_text = f"Commit: {commit_hash}{refs_str}\nAuthor: {values[4]}\nDate: {values[5]}\nSubject: {values[3]}\n\n" + self.details_text.insert(tk.END, header_text) + + if hasattr(self.base, "git") and getattr(self.base.git, "repo", None): + try: + stat = self.base.git.repo.git.show(commit_hash, "--stat", "--oneline") + lines = stat.splitlines() + if len(lines) > 1: + self.details_text.insert(tk.END, "\n".join(lines[1:])) + except Exception: + pass + + self.details_text.config(state=tk.DISABLED) diff --git a/src/biscuit/views/search/results.py b/src/biscuit/views/search/results.py index 88da5e36..b8aa5416 100644 --- a/src/biscuit/views/search/results.py +++ b/src/biscuit/views/search/results.py @@ -237,6 +237,13 @@ def search_in_file(self, file_path: str, search_string: str) -> list: return [file_path, occurrences, line_number] + def refresh_open_editors(self, file_path: str = None) -> None: + if hasattr(self.base, "editorsmanager") and hasattr(self.base.editorsmanager, "editors"): + for editor in getattr(self.base.editorsmanager, "active_editors", []): + if hasattr(editor, "path") and (file_path is None or editor.path == file_path): + if hasattr(editor, "reload"): + editor.reload() + def replace(self) -> None: """ Replace all occurrences from the search with new text diff --git a/src/biscuit/views/search/search.py b/src/biscuit/views/search/search.py index 88d414fe..5301f17d 100644 --- a/src/biscuit/views/search/search.py +++ b/src/biscuit/views/search/search.py @@ -2,7 +2,7 @@ import tkinter.ttk as ttk from biscuit.common.icons import Icons -from biscuit.common.ui import ButtonsEntry, Frame +from biscuit.common.ui import ButtonsEntry, Entry, Frame from ..sidebar_view import SideBarView from .results import Results @@ -50,7 +50,8 @@ def __init__(self, master, *args, **kwargs) -> None: buttons=((Icons.REPLACE_ALL, self.results.replace_normal),), ) - # TODO add ignore folders & extensions boxes + self.exclude_box = Entry(self.container, hint="files to exclude (e.g. *.min.js, node_modules)") + self.exclude_box.pack(fill=tk.X, anchor=tk.N, pady=2) self.container.pack(fill=tk.BOTH, padx=10, pady=5) self.searchbox.pack(fill=tk.X, anchor=tk.N, pady=2) diff --git a/src/biscuit/views/sourcecontrol/placeholder.py b/src/biscuit/views/sourcecontrol/placeholder.py index 931001db..60a2f35e 100644 --- a/src/biscuit/views/sourcecontrol/placeholder.py +++ b/src/biscuit/views/sourcecontrol/placeholder.py @@ -18,7 +18,24 @@ def __init__(self, master, *args, **kwargs) -> None: text="Open a folder containing a git repository." ).grid(row=0, sticky=tk.EW) - # TODO add init repo button if no repo is found for opened folder + self.init_btn = IconLabelButton( + self, + text="Initialize Repository", + icon=Icons.GIT_COMMIT, + callback=self.init_repo, + pady=2, + highlighted=True, + ) + self.init_btn.grid(row=1, pady=5, sticky=tk.EW) + + def init_repo(self, *_) -> None: + if hasattr(self.base, "git") and hasattr(self.base.git, "repo") and hasattr(self.base, "active_directory") and self.base.active_directory: + import git + try: + git.Repo.init(self.base.active_directory) + self.base.git.check_git() + except Exception: + pass # open_btn = IconLabelButton( # self, diff --git a/src/biscuit/views/terminal/queue.py b/src/biscuit/views/terminal/queue.py new file mode 100644 index 00000000..bb0b5c98 --- /dev/null +++ b/src/biscuit/views/terminal/queue.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import queue +from typing import Any, Callable, List, Optional + + +class TerminalCommandQueue: + """Manages command queuing and buffering for terminal execution.""" + + def __init__(self, executor: Optional[Callable[[str], None]] = None): + self.queue: queue.Queue[str] = queue.Queue() + self.executor = executor + self.is_ready: bool = True + + def put(self, command: str) -> None: + if not command: + return + self.queue.put(command) + if self.is_ready: + self.flush() + + def set_ready(self, ready: bool) -> None: + self.is_ready = ready + if self.is_ready: + self.flush() + + def flush(self) -> List[str]: + executed = [] + while not self.queue.empty(): + cmd = self.queue.get() + executed.append(cmd) + if self.executor: + self.executor(cmd) + return executed + + def clear(self) -> None: + while not self.queue.empty(): + self.queue.get_nowait() + + @property + def pending_count(self) -> int: + return self.queue.qsize() diff --git a/src/biscuit/views/terminal/shells/__init__.py b/src/biscuit/views/terminal/shells/__init__.py index ce5986dc..80b8f20c 100644 --- a/src/biscuit/views/terminal/shells/__init__.py +++ b/src/biscuit/views/terminal/shells/__init__.py @@ -1,7 +1,9 @@ -"""Shell classes for the terminal view.""" - +import os import platform +from biscuit.common.icons import Icons +from ..terminalbase import TerminalBase + from .bash import Bash from .cmd import CommandPrompt from .default import Default @@ -14,15 +16,29 @@ elif platform.system() == "Linux": SHELLS.append(Bash) -# def get_shells(base): -# return [(i, lambda i=i: base.open_shell(i)) for i in shells.keys()] -# def get_shell(name): -# return shells.get(name, Default) +class CustomShell(TerminalBase): + """Custom user-configured shell instance.""" + + icon = Icons.TERMINAL + + def __init__(self, master, shell_path: str, name: str = None, *args, **kwargs) -> None: + self.shell = shell_path + self.name = name or os.path.splitext(os.path.basename(shell_path))[0] + super().__init__(master, *args, **kwargs) + self.start_service() + -# def register_shell(shell): -# global shells -# try: -# shells[shell.name] = shell -# except AttributeError: -# shells[f"shell {len(shells) + 1}"] = shell +def create_custom_shell_class(name: str, path: str): + """Factory creating a shell class from name and executable path.""" + return type( + f"CustomShell_{name}", + (CustomShell,), + { + "name": name, + "shell": path, + "__init__": lambda self, master, *a, **kw: CustomShell.__init__( + self, master, path, name=name, *a, **kw + ), + }, + ) diff --git a/src/biscuit/views/terminal/terminal.py b/src/biscuit/views/terminal/terminal.py index e9130e81..8985fbe3 100644 --- a/src/biscuit/views/terminal/terminal.py +++ b/src/biscuit/views/terminal/terminal.py @@ -8,9 +8,10 @@ from ..panelview import PanelView from .menu import TerminalMenu -from .shells import SHELLS, Default +from .shells import SHELLS, Default, create_custom_shell_class from .tabs import Tabs from .terminalbase import TerminalBase +from .queue import TerminalCommandQueue def get_home_directory() -> str: @@ -35,8 +36,7 @@ def __init__(self, master, *args, **kwargs) -> None: self.grid_propagate(False) self.addmenu = TerminalMenu(self, "addterminal") - for i in SHELLS: - self.addmenu.add_command(i.name, lambda i=i: self.open_shell(i)) + self.refresh_shells_menu() self.menu = TerminalMenu(self, "terminal") self.menu.add_command("Clear Terminal", self.clear_terminal) @@ -51,6 +51,9 @@ def __init__(self, master, *args, **kwargs) -> None: self.tabs.grid(row=0, column=1, padx=(1, 0), sticky=tk.NS) self.active_terminals = [] + self.command_queue = TerminalCommandQueue( + lambda cmd: self.active_terminal.run_command(cmd) if self.active_terminal else None + ) self.run_actionset = ActionSet( "Run Command in Terminal", @@ -89,6 +92,27 @@ def open_shell(self, shell) -> None: shell(self, cwd=self.base.active_directory or get_home_directory()) ) + def refresh_shells_menu(self) -> None: + if not hasattr(self, "addmenu") or not self.addmenu: + return + self.addmenu.clear() + for i in SHELLS: + self.addmenu.add_command(i.name, lambda i=i: self.open_shell(i)) + + custom_shells = {} + if hasattr(self.base, "config"): + custom_shells = self.base.config.get_nested("terminal.shells", {}) or {} + + if isinstance(custom_shells, dict): + for name, path in custom_shells.items(): + if isinstance(path, str) and path.strip(): + cls = create_custom_shell_class(name, path.strip()) + self.addmenu.add_command(name, lambda c=cls: self.open_shell(c)) + + def open_custom_shell(self, path: str, name: str = None) -> None: + cls = create_custom_shell_class(name or os.path.splitext(os.path.basename(path))[0], path) + self.open_shell(cls) + def open_terminal(self, path=None) -> None: """Open another instance of active terminal in the current directory. If no active terminal, open a default terminal.""" @@ -131,11 +155,12 @@ def clear_terminal(self, *_) -> None: def run_command(self, command: str) -> None: if not self.active_terminal: - default = self.add_default_terminal() - default.run_command(command) - # this won't work, TODO: implement a queue for commands + self.command_queue.set_ready(False) + self.command_queue.put(command) + self.add_default_terminal() + self.command_queue.set_ready(True) else: - self.active_terminal.run_command(command) + self.command_queue.put(command) def run_external_console(self, command: str) -> None: "Run a command in external console." diff --git a/src/biscuit/views/terminal/terminalbase.py b/src/biscuit/views/terminal/terminalbase.py index 6869eadb..7d52f5b2 100644 --- a/src/biscuit/views/terminal/terminalbase.py +++ b/src/biscuit/views/terminal/terminalbase.py @@ -5,7 +5,10 @@ if os.name == "nt": from winpty import PtyProcess as PTY else: - from ptyprocess import PtyProcessUnicode as PTY + try: + from ptyprocess import PtyProcessUnicode as PTY + except ImportError: + PTY = None from biscuit.common.ui import Scrollbar diff --git a/tests/test_ai_cancellation.py b/tests/test_ai_cancellation.py new file mode 100644 index 00000000..d22a48ae --- /dev/null +++ b/tests/test_ai_cancellation.py @@ -0,0 +1,13 @@ +from unittest.mock import MagicMock, patch +from biscuit.common.ai.agent import Agent + +def test_agent_cancellation(): + base = MagicMock() + with patch("biscuit.common.ai.agent.resolve"): + agent = Agent(base, "key", "test-model") + agent.is_running = True + agent.is_aborted = False + + agent.stop_execution() + assert agent.is_running is False + assert agent.is_aborted is True diff --git a/tests/test_ai_custom_prompt.py b/tests/test_ai_custom_prompt.py new file mode 100644 index 00000000..95d08eec --- /dev/null +++ b/tests/test_ai_custom_prompt.py @@ -0,0 +1,21 @@ +import os +import tempfile +from unittest.mock import MagicMock, patch +from biscuit.common.ai.agent import Agent + +def test_custom_system_prompts(): + base = MagicMock() + with tempfile.TemporaryDirectory() as tmpdir: + biscuit_dir = os.path.join(tmpdir, ".biscuit") + os.makedirs(biscuit_dir) + with open(os.path.join(biscuit_dir, "instructions.md"), "w", encoding="utf-8") as f: + f.write("Always write Python 3.12 syntax.") + + base.active_directory = tmpdir + base.config.get_nested.return_value = "Always be polite." + + with patch("biscuit.common.ai.agent.resolve"): + agent = Agent(base, "key", "test-model") + proj_inst = agent._get_project_instructions() + assert "Always be polite." in proj_inst + assert "Always write Python 3.12 syntax." in proj_inst diff --git a/tests/test_ai_diff_review.py b/tests/test_ai_diff_review.py new file mode 100644 index 00000000..225e65c9 --- /dev/null +++ b/tests/test_ai_diff_review.py @@ -0,0 +1,15 @@ +from biscuit.common.ai.diff_review import DiffReviewManager + +def test_diff_review_staging_and_actions(): + mgr = DiffReviewManager() + edit = mgr.stage_edit("test.py", "def foo(): pass\n", "def foo(): return 42\n") + assert "test.py" in [e.path for e in mgr.list_pending()] + assert "+def foo(): return 42" in edit.diff() + + accepted = mgr.accept_edit("test.py") + assert accepted.path == "test.py" + assert len(mgr.list_pending()) == 0 + + mgr.stage_edit("bar.py", "a = 1\n", "a = 2\n") + mgr.reject_edit("bar.py") + assert len(mgr.list_pending()) == 0 diff --git a/tests/test_ai_inline_completion.py b/tests/test_ai_inline_completion.py new file mode 100644 index 00000000..9663079f --- /dev/null +++ b/tests/test_ai_inline_completion.py @@ -0,0 +1,13 @@ +from biscuit.common.ai.inline_completion import InlineCompletionState + +def test_inline_completion_state(): + state = InlineCompletionState(prefix="", suffix="") + assert not state.is_active + + state.trigger("def foo():", "\nreturn", " print('hello')") + assert state.is_active + assert state.suggestion == " print('hello')" + + accepted = state.accept() + assert accepted == " print('hello')" + assert not state.is_active diff --git a/tests/test_ai_minimax.py b/tests/test_ai_minimax.py new file mode 100644 index 00000000..b7a6c48c --- /dev/null +++ b/tests/test_ai_minimax.py @@ -0,0 +1,373 @@ +"""Exercise the real Anthropic SDK against an offline HTTP transport.""" + +import json +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock + +import anthropic +import httpx +import pytest + +from biscuit.common.ai.agent import Agent +from biscuit.common.ai.providers import _auto_register, list_models, resolve +from biscuit.common.ai.providers.base import ProviderResponse, ToolDesc +from biscuit.common.ai.providers.minimax import MiniMaxProvider +from biscuit.settings.editor.editor import SettingsEditor +from biscuit.views.ai.ai import AI + + +THINKING = { + "type": "thinking", + "thinking": "Check the file.", + "signature": "test-signature", +} +TEXT = {"type": "text", "text": "Done."} +TOOL = { + "type": "tool_use", + "id": "tool_1", + "name": "read_file", + "input": {"path": "a.py"}, +} + + +def message(content): + """Build a complete API message for the offline transport.""" + return { + "id": "msg_test", + "type": "message", + "role": "assistant", + "model": "MiniMax-M3.1-Flash", + "content": content, + "stop_reason": "tool_use" + if any(b["type"] == "tool_use" for b in content) + else "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 12, "output_tokens": 8}, + } + + +def stream_events(content): + """Encode content blocks as a real SDK-compatible event stream.""" + initial = message([]) + initial["stop_reason"] = None + events = [{"type": "message_start", "message": initial}] + for index, block in enumerate(content): + kind = block["type"] + start = dict(block) + if kind == "text": + start["text"] = "" + deltas = [{"type": "text_delta", "text": block["text"]}] + elif kind == "thinking": + start.update(thinking="", signature="") + deltas = [ + {"type": "thinking_delta", "thinking": block["thinking"]}, + {"type": "signature_delta", "signature": block["signature"]}, + ] + else: + start["input"] = {} + deltas = [ + {"type": "input_json_delta", "partial_json": json.dumps(block["input"])} + ] + events.append( + {"type": "content_block_start", "index": index, "content_block": start} + ) + events.extend( + {"type": "content_block_delta", "index": index, "delta": delta} + for delta in deltas + ) + events.append({"type": "content_block_stop", "index": index}) + events.extend( + [ + { + "type": "message_delta", + "delta": { + "stop_reason": message(content)["stop_reason"], + "stop_sequence": None, + }, + "usage": {"output_tokens": 8}, + }, + {"type": "message_stop"}, + ] + ) + return "".join( + f"event: {event['type']}\ndata: {json.dumps(event)}\n\n" for event in events + ) + + +@pytest.fixture +async def transport(monkeypatch): + """Capture HTTP payloads while retaining real SDK serialization.""" + requests = [] + responses = [] + clients = [] + + def handle(request): + """Record a request and return its queued response without network I/O.""" + assert str(request.url) == "https://api.minimax.io/anthropic/v1/messages" + payload = json.loads(request.content) + requests.append(payload) + content = responses.pop(0) if responses else [THINKING, TEXT] + if payload.get("stream"): + return httpx.Response( + 200, + text=stream_events(content), + headers={"content-type": "text/event-stream"}, + ) + return httpx.Response(200, json=message(content)) + + async_cls = anthropic.AsyncAnthropic + sync_cls = anthropic.Anthropic + + def async_client(**kwargs): + """Use the SDK's async client with the offline HTTP transport.""" + client = async_cls( + **kwargs, + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle)), + ) + clients.append(client) + return client + + def sync_client(**kwargs): + """Use the SDK's sync client with the offline HTTP transport.""" + return sync_cls( + **kwargs, http_client=httpx.Client(transport=httpx.MockTransport(handle)) + ) + + monkeypatch.setattr(anthropic, "AsyncAnthropic", async_client) + monkeypatch.setattr(anthropic, "Anthropic", sync_client) + yield requests, responses + for client in clients: + await client.close() + + +def assert_defaults(payload, model): + """Check the selected model and the API's default thinking contract.""" + assert payload["model"] == model + assert payload["max_tokens"] == 8000 + # The API supplies adaptive/max for Flash and thinking-off for M3. + for omitted in ( + "thinking", + "output_config", + "service_tier", + "reasoning_effort", + "temperature", + ): + assert omitted not in payload + + +def test_catalog_and_explicit_models(): + """Keep ordinary choices current without losing explicit model IDs.""" + _auto_register() + assert list(MiniMaxProvider.get_models().values()) == [ + "MiniMax-M3.1-Flash", + "MiniMax-M3", + ] + assert MiniMaxProvider("test-key").model == "MiniMax-M3.1-Flash" + ordinary = list_models() + assert [mid for mid in ordinary.values() if mid.lower().startswith("minimax-")] == [ + "MiniMax-M3.1-Flash", + "MiniMax-M3", + ] + for saved in ("minimax-m1", "minimax-text-01", "MiniMax-M2.7", "MiniMax-custom"): + assert resolve(saved, "test-key").model == saved + assert saved not in ordinary.values() + assert saved in list_models(saved).values() + assert "custom-model" in ordinary.values() + assert resolve("openai-compatible:my-model", "test-key").name == "openai_compatible" + assert ( + list_models("openai-compatible:my-model")["openai-compatible:my-model"] + == "openai-compatible:my-model" + ) + + +@pytest.mark.parametrize( + "model", + [ + None, + "MiniMax-M3", + "minimax-m1", + "minimax-text-01", + "MiniMax-M2.7", + "MiniMax-custom", + ], +) +async def test_sdk_default_and_explicit_requests(transport, model): + """Serialize default and explicit selections through the real async SDK.""" + requests, _ = transport + provider = ( + MiniMaxProvider("test-key") if model is None else resolve(model, "test-key") + ) + result = await provider.generate( + "Be helpful.", [{"role": "user", "content": "Hello"}] + ) + assert result.text == "Done." + assert result.assistant_content == [THINKING, TEXT] + assert (result.input_tokens, result.output_tokens) == (12, 8) + assert_defaults(requests[0], model or "MiniMax-M3.1-Flash") + assert requests[0]["stream"] is True + assert "tools" not in requests[0] + + +@pytest.mark.parametrize("model", ["MiniMax-M3.1-Flash", "MiniMax-M3", "minimax-m1"]) +def test_sync_thinking_first_reply(transport, model): + """Return text after a leading thinking block in the synchronous path.""" + requests, _ = transport + assert resolve(model, "test-key").process_message("Hello") == "Done." + assert_defaults(requests[0], model) + + +@pytest.mark.parametrize( + "saved", + [ + "MiniMax-M3.1-Flash", + "MiniMax-M3", + "minimax-m1", + "MiniMax-custom", + "openai-compatible:my-model", + "gemini-2.5-flash", + "", + ], +) +async def test_saved_sidebar_selection_reaches_transport(transport, saved): + """Resolve sidebar selections to unchanged outgoing model IDs.""" + _auto_register() + view = SimpleNamespace(base=MagicMock()) + view.base.config.get_nested.return_value = saved + models = list_models(saved) + selected = AI._load_default_model(view) + model_id = models[selected] + assert model_id == (saved or next(iter(list_models().values()))) + if model_id.lower().startswith("minimax-"): + await resolve(model_id, "test-key").generate( + "", [{"role": "user", "content": "Hello"}] + ) + assert transport[0][0]["model"] == saved + + +@pytest.mark.parametrize( + "saved", ["minimax-text-01", "MiniMax-custom", "openai-compatible:my-model"] +) +def test_settings_keeps_saved_selection(saved): + """Retain explicit saved IDs when the settings selector is rebuilt.""" + editor = MagicMock() + editor.base.config.get_nested.side_effect = lambda key, default="": ( + saved if key == "ai.default_model" else default + ) + SettingsEditor.add_ai(editor) + args = editor.add_section.return_value.add_dropdown.call_args.args + assert args[1][args[2]] == saved + args[3](saved) + editor.base.config.set_nested.assert_called_once_with("ai.default_model", saved) + + +async def test_sdk_preserves_images_video_and_legacy_tool_messages(transport): + """Preserve media blocks and convert legacy tool messages on the wire.""" + requests, _ = transport + content = [ + {"type": "text", "text": "Describe these"}, + { + "type": "image", + "source": {"type": "url", "url": "https://example.test/image.png"}, + }, + { + "type": "image", + "source": {"type": "base64", "media_type": "image/png", "data": "aW1hZ2U="}, + }, + { + "type": "video", + "source": {"type": "url", "url": "https://example.test/video.mp4"}, + }, + ] + tools = [ + ToolDesc( + "read_file", + "Read a file", + { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + ) + ] + chunks = [] + result = await MiniMaxProvider("test-key").generate( + "", + [ + {"role": "user", "content": content}, + { + "role": "assistant", + "content": "Read it", + "tool_calls": [ + {"id": "tool_1", "name": "read_file", "args": {"path": "a.py"}} + ], + }, + {"role": "tool", "tool_call_id": "tool_1", "content": "file contents"}, + ], + tools=tools, + stream_callback=chunks.append, + ) + assert chunks == ["Done."] + assert result.text == "Done." + assert requests[0]["messages"][0]["content"] == content + assert requests[0]["messages"][1]["content"][1] == TOOL + assert requests[0]["messages"][2]["content"][0]["tool_use_id"] == "tool_1" + assert requests[0]["tools"][0]["input_schema"] == tools[0].input_schema + + +async def test_agent_replays_complete_reasoning_tool_history(transport, monkeypatch): + """Replay signed thinking and tool blocks through loops and later turns.""" + requests, responses = transport + responses.extend([[THINKING, TOOL], [THINKING, TEXT], [THINKING, TEXT]]) + base = MagicMock() + base.active_directory = None + agent = Agent(base, "test-key", "MiniMax-M3.1-Flash") + monkeypatch.setattr(agent, "_get_system_instruction", lambda: "Be helpful.") + tool = MagicMock() + tool.name = "read_file" + tool.description = "Read a file" + tool.args_schema.schema.return_value = { + "properties": {"path": {"type": "string"}}, + "required": ["path"], + } + tool.run.return_value = "file contents" + agent.tools = [tool] + first = await agent.execute_task_with_streaming("Read a.py") + assert first.status.name == "COMPLETED" + assert len(requests) == 2 + assert requests[1]["messages"][1] == { + "role": "assistant", + "content": [THINKING, TOOL], + } + assert requests[1]["messages"][2]["content"] == [ + {"type": "tool_result", "tool_use_id": "tool_1", "content": "file contents"}, + ] + await agent.execute_task_with_streaming("What next?") + assert len(requests) == 3 + assert requests[2]["messages"][:3] == requests[1]["messages"] + assert requests[2]["messages"][3] == { + "role": "assistant", + "content": [THINKING, TEXT], + } + assert requests[2]["messages"][4] == {"role": "user", "content": "What next?"} + assert ProviderResponse(text="Plain reply").assistant_content is None + + +async def test_plain_provider_keeps_existing_text_history(monkeypatch): + """Leave other providers' plain-text conversation history unchanged.""" + base = MagicMock() + base.active_directory = None + agent = Agent(base, "test-key", "custom-model") + monkeypatch.setattr(agent, "_get_system_instruction", lambda: "Be helpful.") + agent.tools = [] + agent.provider.generate = AsyncMock( + return_value=ProviderResponse(text="Plain reply") + ) + await agent.execute_task_with_streaming("Hello") + await agent.execute_task_with_streaming("Again") + assert agent._native_history == [] + messages = agent.provider.generate.call_args.kwargs["messages"] + assert messages[:3] == [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Plain reply"}, + {"role": "user", "content": "Again"}, + ] diff --git a/tests/test_ai_ollama.py b/tests/test_ai_ollama.py new file mode 100644 index 00000000..d13cc551 --- /dev/null +++ b/tests/test_ai_ollama.py @@ -0,0 +1,68 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + +from biscuit.common.ai.providers import get_provider_for_model, resolve +from biscuit.common.ai.providers.ollama import OllamaProvider +from biscuit.common.ai.providers.base import ToolDesc + + +def test_ollama_provider_registered(): + provider_cls = get_provider_for_model("llama3.2") + assert provider_cls is OllamaProvider + assert OllamaProvider.supports_model("qwen2.5-coder") + assert OllamaProvider.supports_model("codellama") + + instance = resolve("llama3.2", api_key="") + assert isinstance(instance, OllamaProvider) + assert instance.model == "llama3.2" + assert instance.base_url == "http://localhost:11434/v1" + + +def test_ollama_build_messages_and_tools(): + provider = OllamaProvider(model="llama3.2", base_url="http://custom:11434") + assert provider.base_url == "http://custom:11434/v1" + + messages = [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "thinking", "tool_calls": [{"name": "test_tool", "args": {"arg1": "val1"}, "id": "123"}]}, + {"role": "tool", "content": "result", "tool_call_id": "123"}, + ] + built = provider._build_messages("You are helpful", messages) + assert len(built) == 4 + assert built[0]["role"] == "system" + assert built[1]["role"] == "user" + assert built[2]["role"] == "assistant" + assert built[2]["tool_calls"][0]["function"]["name"] == "test_tool" + assert built[3]["role"] == "tool" + + tools = [ToolDesc(name="calculator", description="math", input_schema={"type": "object"})] + converted = provider._convert_tools(tools) + assert len(converted) == 1 + assert converted[0]["function"]["name"] == "calculator" + + +@pytest.mark.asyncio +async def test_ollama_generate_mock(): + provider = OllamaProvider(model="llama3.2") + + mock_resp_data = { + "choices": [{ + "message": { + "role": "assistant", + "content": "Hello from Ollama!", + "tool_calls": [] + } + }], + "usage": {"prompt_tokens": 10, "completion_tokens": 5} + } + + with patch("httpx.AsyncClient.post") as mock_post: + mock_response = MagicMock() + mock_response.json.return_value = mock_resp_data + mock_response.raise_for_status = MagicMock() + mock_post.return_value = mock_response + + res = await provider.generate("system prompt", [{"role": "user", "content": "hi"}]) + assert res.text == "Hello from Ollama!" + assert res.input_tokens == 10 + assert res.output_tokens == 5 diff --git a/tests/test_ai_openai_compatible.py b/tests/test_ai_openai_compatible.py new file mode 100644 index 00000000..c2f85692 --- /dev/null +++ b/tests/test_ai_openai_compatible.py @@ -0,0 +1,8 @@ +from biscuit.common.ai.providers.openai_compatible import OpenAICompatibleProvider +from biscuit.common.ai.providers import resolve + +def test_openai_compatible_provider(): + p = OpenAICompatibleProvider(api_key="sk-test", model="custom-model", base_url="http://vllm-server:8000/v1") + assert p.base_url == "http://vllm-server:8000/v1" + assert OpenAICompatibleProvider.supports_model("openai-compatible:my-custom-llm") + assert p.supports_model("custom-model") diff --git a/tests/test_ai_rate_limit_retry.py b/tests/test_ai_rate_limit_retry.py new file mode 100644 index 00000000..52722658 --- /dev/null +++ b/tests/test_ai_rate_limit_retry.py @@ -0,0 +1,16 @@ +import pytest +from biscuit.common.ai.retry import retry_with_backoff + +@pytest.mark.asyncio +async def test_retry_on_429(): + attempts = 0 + async def flaky_call(): + nonlocal attempts + attempts += 1 + if attempts < 3: + raise RuntimeError("HTTP 429: Too Many Requests") + return "success" + + res = await retry_with_backoff(flaky_call, max_retries=3, base_delay=0.01) + assert res == "success" + assert attempts == 3 diff --git a/tests/test_ai_semantic_index.py b/tests/test_ai_semantic_index.py new file mode 100644 index 00000000..0e5df003 --- /dev/null +++ b/tests/test_ai_semantic_index.py @@ -0,0 +1,14 @@ +from biscuit.common.ai.semantic_index import SemanticCodeIndex + +def test_semantic_code_indexing(): + idx = SemanticCodeIndex() + idx.index_chunk("src/app.py", 1, 10, "class Application: def run(self): pass") + idx.index_chunk("src/utils.py", 1, 5, "def calculate_sum(a, b): return a + b") + + res = idx.search("Application run") + assert len(res) == 1 + assert res[0][0] == "src/app.py" + + res2 = idx.search("calculate sum") + assert len(res2) == 1 + assert res2[0][0] == "src/utils.py" diff --git a/tests/test_ai_token_budget.py b/tests/test_ai_token_budget.py new file mode 100644 index 00000000..e35aaeef --- /dev/null +++ b/tests/test_ai_token_budget.py @@ -0,0 +1,12 @@ +from biscuit.common.ai.token_tracker import TokenTracker + +def test_token_tracker_and_budget(): + tracker = TokenTracker(budget_tokens=1000) + tracker.record_usage(400, 300) + assert tracker.total_tokens == 700 + assert tracker.is_budget_exceeded is False + assert tracker.get_summary() == "700/1000 tokens" + + tracker.record_usage(200, 200) + assert tracker.total_tokens == 1100 + assert tracker.is_budget_exceeded is True diff --git a/tests/test_ai_web_search.py b/tests/test_ai_web_search.py new file mode 100644 index 00000000..8882ae7c --- /dev/null +++ b/tests/test_ai_web_search.py @@ -0,0 +1,8 @@ +from biscuit.common.ai.websearch import WebSearchTool + +def test_web_search_tool(): + tool = WebSearchTool() + assert tool.name == "web_search" + res = tool.run({"query": "requests"}) + assert "requests" in res + assert "pypi.org" in res diff --git a/tests/test_api.py b/tests/test_api.py index 9cc74f42..6c293185 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -108,3 +108,28 @@ def test_releases_init(self, mock_base): def test_assistant_init(self, mock_base): a = Assistant(mock_base) assert a.base == mock_base + + def test_settings_endpoint(self, mock_base): + from biscuit.api.settings import Settings + + mock_base.settings = MagicMock() + mock_base.settings.config = MagicMock() + mock_base.settings.config.data = {"existing_key": "val1"} + mock_base.settings.config.get_value.side_effect = lambda k, d=None: mock_base.settings.config.data.get(k, d) + + s = Settings(mock_base) + assert s.get("existing_key") == "val1" + assert s.get("missing_key", "default_val") == "default_val" + + s.set("new_key", "val2") + assert mock_base.settings.config.data["new_key"] == "val2" + + s.register_setting("my_ext.enabled", "boolean", True, "Enable my extension") + registered = s.get_registered_settings() + assert "my_ext.enabled" in registered + assert registered["my_ext.enabled"]["default"] is True + assert mock_base.settings.config.data["my_ext.enabled"] is True + + all_settings = s.get_all() + assert "existing_key" in all_settings + diff --git a/tests/test_autocomplete_doc_popup.py b/tests/test_autocomplete_doc_popup.py new file mode 100644 index 00000000..9bdb90d4 --- /dev/null +++ b/tests/test_autocomplete_doc_popup.py @@ -0,0 +1,10 @@ +from unittest.mock import MagicMock +from biscuit.editor.autocomplete.item import CompletionItem + +def test_completion_item_documentation(): + ci = MagicMock(spec=CompletionItem) + ci.documentation = "Computes the square root of x." + ci.detail = "sqrt(x: float) -> float" + + doc = CompletionItem.get_documentation_text(ci) + assert doc == "Computes the square root of x." diff --git a/tests/test_bracket_colorization.py b/tests/test_bracket_colorization.py new file mode 100644 index 00000000..88db46a0 --- /dev/null +++ b/tests/test_bracket_colorization.py @@ -0,0 +1,80 @@ +import pytest +from biscuit.editor.text.ts_highlighter import TREE_SITTER_AVAILABLE, TreeSitterHighlighter + + +class MockEditorsTheme: + bracket_colors = ("ffd700", "da70d6", "179fff") + activebracket = "#333333" + + +class MockTheme: + treesitter_syntax = {} + editors = MockEditorsTheme() + + +class MockBase: + theme = MockTheme() + + +class MockText: + def __init__(self, content=""): + self.content = content + self.tags = {} + self.tag_configs = {} + self.base = MockBase() + + def get_all_text(self): + return self.content + + def tag_configure(self, tag, **kwargs): + self.tag_configs[tag] = kwargs + + def tag_raise(self, tag, above_this=None): + pass + + def tag_add(self, tag, start, end): + self.tags.setdefault(tag, []).append((start, end)) + + def tag_remove(self, tag, start, end): + if tag in self.tags: + self.tags[tag] = [] + + +@pytest.mark.skipif(not TREE_SITTER_AVAILABLE, reason="tree-sitter not available") +class TestBracketPairColorization: + def test_bracket_colorization_and_matching(self): + code = "def foo(a, b):\n return [1, {2: (3)}]\n" + text = MockText(code) + highlighter = TreeSitterHighlighter(text, language="python") + highlighter.highlight() + + # Check tag configurations for depths + assert "bracket.depth0" in text.tag_configs + assert "bracket.depth1" in text.tag_configs + assert "bracket.depth2" in text.tag_configs + + # Ensure brackets were tagged + depth0_tags = text.tags.get("bracket.depth0", []) + assert len(depth0_tags) >= 2 # def foo(a, b) parens and/or outermost brackets + + # Check bracket map for matching coordinates + # Line 1: def foo(a, b): -> '(' is at (1, 7), ')' is at (1, 12) + matching_close = highlighter.get_matching_bracket(1, 7) + assert matching_close == (1, 12) + assert highlighter.get_matching_bracket(1, 12) == (1, 7) + + # Line 2: return [1, {2: (3)}] + # '[' is at (2, 11), ']' is at (2, 23) + assert highlighter.get_matching_bracket(2, 11) == (2, 23) + assert highlighter.get_matching_bracket(2, 23) == (2, 11) + + def test_clear_removes_bracket_tags(self): + code = "x = (1 + 2)\n" + text = MockText(code) + highlighter = TreeSitterHighlighter(text, language="python") + highlighter.highlight() + assert len(highlighter.bracket_map) > 0 + + highlighter.clear() + assert len(highlighter.bracket_map) == 0 + assert text.tags.get("bracket.depth0") == [] diff --git a/tests/test_cli_extension_version.py b/tests/test_cli_extension_version.py new file mode 100644 index 00000000..b3fa43b1 --- /dev/null +++ b/tests/test_cli_extension_version.py @@ -0,0 +1,28 @@ +from unittest.mock import MagicMock, patch +from biscuit.cli.extensions import info, list_ext + + +def test_cli_info_version_display(): + mock_app = MagicMock() + mock_app.extensions_manager.find_extension_by_name.return_value = ( + "repo_url", "author_name", "Test description", "1.2.3" + ) + + cmd_fn = info.callback("test_ext") + with patch("click.echo") as mock_echo: + cmd_fn(mock_app) + echo_calls = [call.args[0] for call in mock_echo.call_args_list] + assert "Version: 1.2.3" in echo_calls + + +def test_cli_list_installed_version_display(): + mock_app = MagicMock() + mock_app.extensions_manager.list_installed_extensions.return_value = [ + ("my_ext", ("url", "author", "desc", "2.0.0")) + ] + + cmd_fn = list_ext.callback(user=None, installed=True) + with patch("click.echo") as mock_echo: + cmd_fn(mock_app) + echo_calls = [call.args[0] for call in mock_echo.call_args_list] + assert any("[0] my_ext [v2.0.0]:" in line for line in echo_calls) diff --git a/tests/test_commands.py b/tests/test_commands.py index c5b7879f..5584e5ab 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -160,3 +160,16 @@ def test_split_editor(self, commands): def test_show_terminal(self, commands): commands.show_terminal() commands.base.panel.show_terminal.assert_called_once() + + def test_safe_dialog_options(self, commands): + commands.base.theme = MagicMock() + commands.base.theme.secondary_background = "#252526" + commands.base.theme.primary_foreground = "#cccccc" + commands.base.option_add = MagicMock() + + opts = commands._safe_dialog_options(title="Test") + assert opts["parent"] == commands.base + assert opts["title"] == "Test" + commands.base.option_add.assert_any_call("*Dialog*background", "#252526") + commands.base.option_add.assert_any_call("*Dialog*foreground", "#cccccc") + diff --git a/tests/test_dap.py b/tests/test_dap.py new file mode 100644 index 00000000..eecd61d0 --- /dev/null +++ b/tests/test_dap.py @@ -0,0 +1,363 @@ +import json +from unittest.mock import MagicMock + +import pytest + +from biscuit.debugger.dap import DAPDebugger, DAPProtocol +from biscuit.debugger.manager import DebuggerManager + + +class TestDAPProtocol: + def test_build_request(self): + proto = DAPProtocol() + req_bytes = proto.build_request("initialize", {"clientID": "biscuit"}) + assert req_bytes.startswith(b"Content-Length: ") + assert b"\r\n\r\n" in req_bytes + header, body = req_bytes.split(b"\r\n\r\n", 1) + length = int(header.decode().split(": ")[1]) + assert len(body) == length + + data = json.loads(body.decode()) + assert data["seq"] == 1 + assert data["type"] == "request" + assert data["command"] == "initialize" + assert data["arguments"]["clientID"] == "biscuit" + + def test_build_response(self): + proto = DAPProtocol() + resp_bytes = proto.build_response( + request_seq=1, command="initialize", success=True, body={"supportsConfigurationDone": True} + ) + header, body = resp_bytes.split(b"\r\n\r\n", 1) + data = json.loads(body.decode()) + assert data["type"] == "response" + assert data["request_seq"] == 1 + assert data["command"] == "initialize" + assert data["success"] is True + assert data["body"]["supportsConfigurationDone"] is True + + def test_build_event(self): + proto = DAPProtocol() + evt_bytes = proto.build_event("stopped", {"reason": "breakpoint", "threadId": 1}) + header, body = evt_bytes.split(b"\r\n\r\n", 1) + data = json.loads(body.decode()) + assert data["type"] == "event" + assert data["event"] == "stopped" + assert data["body"]["reason"] == "breakpoint" + + def test_feed_data_complete(self): + proto = DAPProtocol() + req_bytes = proto.build_request("next", {"threadId": 1}) + messages = proto.feed_data(req_bytes) + assert len(messages) == 1 + assert messages[0]["command"] == "next" + + def test_feed_data_fragmented(self): + proto = DAPProtocol() + req_bytes = proto.build_request("pause", {"threadId": 2}) + + # Feed byte by byte + messages = [] + for b in req_bytes: + messages.extend(proto.feed_data(bytes([b]))) + + assert len(messages) == 1 + assert messages[0]["command"] == "pause" + assert messages[0]["arguments"]["threadId"] == 2 + + def test_feed_data_multiple(self): + proto = DAPProtocol() + req1 = proto.build_request("stepIn", {"threadId": 1}) + req2 = proto.build_request("stepOut", {"threadId": 1}) + + messages = proto.feed_data(req1 + req2) + assert len(messages) == 2 + assert messages[0]["command"] == "stepIn" + assert messages[1]["command"] == "stepOut" + + +class TestDAPDebugger: + def test_init_and_registration(self): + mock_base = MagicMock() + mock_base.secondary_sidebar.debug.variables = MagicMock() + mock_base.secondary_sidebar.debug.callstack = MagicMock() + manager = DebuggerManager(mock_base) + + assert "dap" in manager.debuggers + assert manager.debuggers["dap"] == DAPDebugger + + debugger = manager.request_debugger("dap") + assert isinstance(debugger, DAPDebugger) + + def test_handle_event_initialized(self): + mock_base = MagicMock() + mock_base.secondary_sidebar.debug.variables = MagicMock() + mock_base.secondary_sidebar.debug.callstack = MagicMock() + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + debugger.send_request = MagicMock() + debugger.breakpoints = {"main.py": {10, 20}} + debugger.program_path = "main.py" + debugger.cwd = "." + + debugger.handle_event("initialized", {}) + + # Should have sent setBreakpoints, configurationDone, launch + commands_sent = [call.args[0] for call in debugger.send_request.call_args_list] + assert "setBreakpoints" in commands_sent + assert "configurationDone" in commands_sent + assert "launch" in commands_sent + + def test_handle_event_stopped_and_stacktrace(self): + mock_base = MagicMock() + mock_callstack = MagicMock() + mock_variables = MagicMock() + mock_base.secondary_sidebar.debug.callstack = mock_callstack + mock_base.secondary_sidebar.debug.variables = mock_variables + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + debugger.send_request = MagicMock() + + debugger.handle_event("stopped", {"reason": "breakpoint", "threadId": 3}) + assert debugger.is_paused is True + assert debugger.current_thread_id == 3 + debugger.send_request.assert_called_with("stackTrace", {"threadId": 3}) + + # Test stackTrace response handling + debugger.callstack = mock_callstack + debugger.variables = mock_variables + debugger.handle_response( + "stackTrace", + { + "success": True, + "body": { + "stackFrames": [ + { + "id": 100, + "name": "my_function", + "source": {"path": "/path/to/main.py"}, + "line": 42, + } + ] + }, + }, + ) + mock_callstack.show.assert_called_once_with([("my_function", "/path/to/main.py", 42)]) + assert debugger.current_frame_id == 100 + + def test_dap_client_integration(self): + mock_base = MagicMock() + mock_base.secondary_sidebar.debug.variables = MagicMock() + mock_base.secondary_sidebar.debug.callstack = MagicMock() + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + + # Check client integration + assert debugger.client is not None + # Send request through DAPClient + seq = debugger.send_request("next", {"threadId": 1}) + assert seq == 1 + assert debugger.pending_requests[seq] == "next" + + def test_handle_client_event_event_and_response(self): + mock_base = MagicMock() + mock_base.secondary_sidebar.debug.variables = MagicMock() + mock_base.secondary_sidebar.debug.callstack = MagicMock() + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + debugger.handle_event = MagicMock() + debugger.handle_response = MagicMock() + + try: + from dap.protocol import Event + except ImportError: + from biscuit.common.dap.protocol import Event + + # Test event dispatch + evt = Event(seq=1, type="event", event="stopped", body={"threadId": 2}) + debugger.handle_client_event(evt) + debugger.handle_event.assert_called_once_with("stopped", {"threadId": 2}) + + # Test response dispatch + resp_evt = Event( + seq=2, + type="event", + event="response", + body={"command": "stackTrace", "success": True, "body": {}}, + ) + debugger.handle_client_event(resp_evt) + debugger.handle_response.assert_called_once_with("stackTrace", resp_evt.body) + + def test_stepping_and_continue_pause(self): + mock_base = MagicMock() + mock_base.secondary_sidebar.debug.variables = MagicMock() + mock_base.secondary_sidebar.debug.callstack = MagicMock() + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + debugger.send_request = MagicMock() + + # When paused + debugger.is_paused = True + debugger.current_thread_id = 5 + + debugger.step_in() + debugger.send_request.assert_called_with("stepIn", {"threadId": 5}) + + debugger.step_over() + debugger.send_request.assert_called_with("next", {"threadId": 5}) + + debugger.step_out() + debugger.send_request.assert_called_with("stepOut", {"threadId": 5}) + + debugger.continue_pause() + debugger.send_request.assert_called_with("continue", {"threadId": 5}) + assert debugger.is_paused is False + + debugger.continue_pause() + debugger.send_request.assert_called_with("pause", {"threadId": 5}) + + def test_evaluate_and_variable(self): + mock_base = MagicMock() + mock_base.secondary_sidebar.debug.variables = MagicMock() + mock_base.secondary_sidebar.debug.callstack = MagicMock() + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + debugger.send_request = MagicMock() + + debugger.current_variables_ref = 42 + debugger.set_variable("foo", "bar") + debugger.send_request.assert_called_with( + "setVariable", {"variablesReference": 42, "name": "foo", "value": "bar"} + ) + + debugger.current_frame_id = 7 + debugger.evaluate("x + 1") + debugger.send_request.assert_called_with( + "evaluate", {"expression": "x + 1", "frameId": 7} + ) + + def test_register_dap_adapter(self): + mock_base = MagicMock() + manager = DebuggerManager(mock_base) + + manager.register_dap_adapter("rust", ["lldb-dap", "--port", "1234"]) + assert "rust" in manager.debuggers + + rust_debugger = manager.request_debugger("rust") + assert isinstance(rust_debugger, DAPDebugger) + assert rust_debugger.adapter_command == ["lldb-dap", "--port", "1234"] + + def test_python_debugger_is_dap(self): + from biscuit.debugger.python import PythonDebugger + mock_base = MagicMock() + manager = DebuggerManager(mock_base) + + py_dbg = manager.request_debugger("python") + assert isinstance(py_dbg, DAPDebugger) + assert isinstance(py_dbg, PythonDebugger) + assert "debugpy.adapter" in " ".join(py_dbg.adapter_command) + + def test_duplicate_initialized_ignored(self): + mock_base = MagicMock() + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + debugger.send_request = MagicMock() + debugger.breakpoints = {"main.py": {10}} + + debugger.handle_event("initialized", {}) + count_first = len(debugger.send_request.call_args_list) + assert count_first >= 3 # launch, setBreakpoints, configurationDone + + # Second initialized (e.g. from adapter server) must be ignored + debugger.handle_event("initialized", {}) + assert len(debugger.send_request.call_args_list) == count_first + + def test_conditional_breakpoint_serialization(self): + from biscuit.debugger.breakpoints import Breakpoint + mock_base = MagicMock() + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + debugger.send_request = MagicMock() + + bp = Breakpoint(line=15, condition="x > 10", hit_condition=2) + debugger.breakpoints = {"main.py": [bp]} + + debugger.handle_event("initialized", {}) + bp_calls = [ + c for c in debugger.send_request.call_args_list if c.args[0] == "setBreakpoints" + ] + assert len(bp_calls) == 1 + bps = bp_calls[0].args[1]["breakpoints"] + assert bps[0]["line"] == 15 + assert bps[0]["condition"] == "x > 10" + assert bps[0]["hitCondition"] == "2" + + def test_threads_manager_integration(self): + mock_base = MagicMock() + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + + debugger.handle_event("thread", {"reason": "started", "threadId": 10}) + assert 10 in debugger.threads_manager.threads + + debugger.handle_event("thread", {"reason": "exited", "threadId": 10}) + assert 10 not in debugger.threads_manager.threads + + @pytest.mark.integration + def test_real_debugpy_end_to_end(self, tmp_path): + import time + script = tmp_path / "target.py" + script.write_text("x = 42\ny = x + 10\nprint('Done', y)\n") + + mock_base = MagicMock() + mock_callstack = MagicMock() + mock_variables = MagicMock() + mock_base.secondary_sidebar.debug.callstack = mock_callstack + mock_base.secondary_sidebar.debug.variables = mock_variables + mock_base.debug.reset = MagicMock() + mock_base.debug.set_paused = MagicMock() + mock_base.debug.set_running = MagicMock() + mock_base.debug.set_stopped = MagicMock() + mock_base.debug.refresh = MagicMock() + + manager = DebuggerManager(mock_base) + debugger = DAPDebugger(manager) + debugger.callstack = mock_callstack + debugger.variables = mock_variables + debugger.breakpoints = {str(script): {2}} + + debugger.launch(str(script), str(tmp_path)) + + # Wait up to 10 seconds for breakpoint hit + start = time.time() + while time.time() - start < 10: + if debugger.is_paused: + break + time.sleep(0.1) + + assert debugger.is_paused is True + assert debugger.current_thread_id is not None + + # Wait for variables inspection + start = time.time() + while time.time() - start < 10: + if mock_variables.show.called: + break + time.sleep(0.1) + + assert mock_variables.show.called + # Check that x = 42 was inspected + found_x = False + for call in mock_variables.show.call_args_list: + vars_dict = call.args[1] if len(call.args) > 1 else call.kwargs.get("d", {}) + if "x" in vars_dict and str(vars_dict["x"]) == "42": + found_x = True + break + assert found_x, f"Expected variable 'x' == '42' in {mock_variables.show.call_args_list}" + + # Resume execution + debugger.continue_pause() + time.sleep(0.5) + debugger.stop() + assert debugger.is_running is False + diff --git a/tests/test_debugger_conditional_breakpoints.py b/tests/test_debugger_conditional_breakpoints.py new file mode 100644 index 00000000..1914660f --- /dev/null +++ b/tests/test_debugger_conditional_breakpoints.py @@ -0,0 +1,25 @@ +from biscuit.debugger.breakpoints import Breakpoint, BreakpointManager + + +def test_standard_breakpoint(): + mgr = BreakpointManager() + mgr.set_breakpoint("test.py", 10) + assert mgr.get_lines("test.py") == {10} + assert mgr.should_pause_at("test.py", 10) is True + + +def test_conditional_breakpoint(): + mgr = BreakpointManager() + mgr.set_breakpoint("test.py", 20, condition="x > 5") + + assert mgr.should_pause_at("test.py", 20, {"x": 3}) is False + assert mgr.should_pause_at("test.py", 20, {"x": 6}) is True + + +def test_hit_count_breakpoint(): + mgr = BreakpointManager() + mgr.set_breakpoint("test.py", 30, hit_condition=3) + + assert mgr.should_pause_at("test.py", 30) is False # 1st hit + assert mgr.should_pause_at("test.py", 30) is False # 2nd hit + assert mgr.should_pause_at("test.py", 30) is True # 3rd hit diff --git a/tests/test_debugger_config.py b/tests/test_debugger_config.py new file mode 100644 index 00000000..c0f6861d --- /dev/null +++ b/tests/test_debugger_config.py @@ -0,0 +1,56 @@ +from pathlib import Path +from unittest.mock import MagicMock, patch +import pytest + +from biscuit.debugger.config import ConfigLoader + + +class TestDebuggerConfigLoader: + def test_check_clears_configs_when_no_active_directory(self): + manager = MagicMock() + manager.base = MagicMock() + manager.base.active_directory = None + + loader = ConfigLoader(manager) + loader.configs = [{"name": "Old Config"}] + loader.config_found = True + + loader.check() + + assert loader.config_found is False + assert loader.configs == [] + manager.base.debug.refresh.assert_called() + manager.base.debug.refresh_configs.assert_called() + + def test_check_clears_configs_when_config_file_not_found(self, tmp_path): + manager = MagicMock() + manager.base = MagicMock() + manager.base.active_directory = str(tmp_path) + + loader = ConfigLoader(manager) + loader.configs = [{"name": "Old Config"}] + loader.config_found = True + + loader.check() + + assert loader.config_found is False + assert loader.configs == [] + manager.base.debug.refresh.assert_called() + manager.base.debug.refresh_configs.assert_called() + + def test_check_loads_configs_when_file_exists(self, tmp_path): + manager = MagicMock() + manager.base = MagicMock() + manager.base.active_directory = str(tmp_path) + + biscuit_dir = tmp_path / ".biscuit" + biscuit_dir.mkdir() + launch_file = biscuit_dir / "launch.toml" + launch_file.write_text('[debug1]\nname = "Python: Current File"\n') + + loader = ConfigLoader(manager) + loader.check() + + assert loader.config_found is True + assert len(list(loader.configs)) == 1 + manager.base.debug.refresh_configs.assert_called() diff --git a/tests/test_debugger_console.py b/tests/test_debugger_console.py new file mode 100644 index 00000000..815d9ae9 --- /dev/null +++ b/tests/test_debugger_console.py @@ -0,0 +1,23 @@ +from biscuit.debugger.console import DebugConsoleSession + + +def test_console_eval_expression(): + session = DebugConsoleSession() + res = session.evaluate("1 + 2 * 3") + assert res.output == "7" + assert res.is_error is False + + +def test_console_eval_scope(): + session = DebugConsoleSession() + scope = {"user": "Alice", "score": 95} + res = session.evaluate("f'{user} has score {score}'", scope=scope) + assert res.output == "'Alice has score 95'" + assert res.is_error is False + + +def test_console_eval_error(): + session = DebugConsoleSession() + res = session.evaluate("1 / 0") + assert res.is_error is True + assert "ZeroDivisionError" in res.output diff --git a/tests/test_debugger_exceptions.py b/tests/test_debugger_exceptions.py new file mode 100644 index 00000000..057a6c06 --- /dev/null +++ b/tests/test_debugger_exceptions.py @@ -0,0 +1,15 @@ +from biscuit.debugger.exceptions import ExceptionBreakpointController + + +def test_exception_breakpoint_controller_defaults(): + ctrl = ExceptionBreakpointController() + assert ctrl.should_break_on_exception(is_uncaught=True) is True + assert ctrl.should_break_on_exception(is_uncaught=False) is False + assert ctrl.get_dap_filters() == ["uncaught"] + + +def test_exception_breakpoint_controller_all(): + ctrl = ExceptionBreakpointController(break_on_uncaught=False, break_on_all=True) + assert ctrl.should_break_on_exception(is_uncaught=False) is True + assert ctrl.should_break_on_exception(is_uncaught=True) is True + assert "all" in ctrl.get_dap_filters() diff --git a/tests/test_debugger_threads.py b/tests/test_debugger_threads.py new file mode 100644 index 00000000..03eddb49 --- /dev/null +++ b/tests/test_debugger_threads.py @@ -0,0 +1,20 @@ +from biscuit.debugger.threads import StackFrame, ThreadCallStackManager, ThreadInfo + + +def test_thread_callstack_manager(): + mgr = ThreadCallStackManager() + f1 = StackFrame(id=1, name="worker_run", file="worker.py", line=42) + f2 = StackFrame(id=2, name="main", file="app.py", line=10) + + t1 = ThreadInfo(id=101, name="WorkerThread-1", status="paused", frames=[f1]) + t2 = ThreadInfo(id=102, name="MainThread", status="running", frames=[f2]) + + mgr.update_threads([t1, t2]) + assert mgr.active_thread_id == 101 + assert mgr.get_active_frames() == [f1] + + # Switch thread + selected = mgr.select_thread(102) + assert selected == t2 + assert mgr.active_thread_id == 102 + assert mgr.get_active_frames() == [f2] diff --git a/tests/test_debugger_watch.py b/tests/test_debugger_watch.py new file mode 100644 index 00000000..4471b93c --- /dev/null +++ b/tests/test_debugger_watch.py @@ -0,0 +1,22 @@ +from biscuit.views.debug.watch import WatchManager + + +def test_watch_manager_add_and_remove(): + mgr = WatchManager() + mgr.add_expression("len(items)") + mgr.add_expression("x + y") + + assert mgr.expressions == ["len(items)", "x + y"] + + mgr.remove_expression("x + y") + assert mgr.expressions == ["len(items)"] + + +def test_watch_manager_evaluate_all(): + mgr = WatchManager() + mgr.add_expression("x * 2") + mgr.add_expression("undefined_var") + + results = mgr.evaluate_all({"x": 21}) + assert results["x * 2"] == "42" + assert "= 1 + assert folds[0] == (1, 3) diff --git a/tests/test_editor_hover_delay.py b/tests/test_editor_hover_delay.py new file mode 100644 index 00000000..6c4fddf7 --- /dev/null +++ b/tests/test_editor_hover_delay.py @@ -0,0 +1,14 @@ +from unittest.mock import MagicMock +from biscuit.editor.text.text import Text + +def test_delayed_hover_dismissal(): + t = MagicMock(spec=Text) + t._hover_hide_timer = "timer_123" + t.hover = MagicMock() + + Text.cancel_hide_hover(t) + t.after_cancel.assert_called_with("timer_123") + assert t._hover_hide_timer is None + + Text._dismiss_hover_if_outside(t) + t.hover.hide.assert_called_once() diff --git a/tests/test_editor_html_browser.py b/tests/test_editor_html_browser.py new file mode 100644 index 00000000..ad762f84 --- /dev/null +++ b/tests/test_editor_html_browser.py @@ -0,0 +1,10 @@ +from unittest.mock import MagicMock, patch +from biscuit.editor.html.editor import HTMLEditor + +def test_html_open_in_browser(): + ed = MagicMock(spec=HTMLEditor) + ed.path = "/tmp/test.html" + + with patch("webbrowser.open_new_tab") as mock_open: + HTMLEditor.open_in_browser(ed) + mock_open.assert_called_once_with("file:///tmp/test.html") diff --git a/tests/test_editor_minimap_yview.py b/tests/test_editor_minimap_yview.py new file mode 100644 index 00000000..9f97ecb6 --- /dev/null +++ b/tests/test_editor_minimap_yview.py @@ -0,0 +1,12 @@ +from unittest.mock import MagicMock +from biscuit.editor.text.minimap import Minimap + +def test_minimap_smooth_yview_sync(): + m = MagicMock(spec=Minimap) + m.cw = MagicMock() + m.cw.winfo_height.return_value = 500 + m.yvalue = 0 + + Minimap.update_from_yview(m, 0.25, 0.75) + assert m.yvalue == 125 + m.cw.coords.assert_called_with("slider", 0, 125) diff --git a/tests/test_editor_multi_cursor.py b/tests/test_editor_multi_cursor.py new file mode 100644 index 00000000..ae786ef0 --- /dev/null +++ b/tests/test_editor_multi_cursor.py @@ -0,0 +1,15 @@ +from unittest.mock import MagicMock +from biscuit.editor.text.text import Text + +def test_multi_cursor_support(): + # Test multi cursor tracking methods + t = MagicMock(spec=Text) + t.cursors = [] + Text.add_cursor(t, "1.5") + Text.add_cursor(t, "2.10") + assert len(t.cursors) == 2 + assert "1.5" in t.cursors + assert "2.10" in t.cursors + + Text.clear_cursors(t) + assert len(t.cursors) == 0 diff --git a/tests/test_editor_sticky_scroll.py b/tests/test_editor_sticky_scroll.py new file mode 100644 index 00000000..19186d67 --- /dev/null +++ b/tests/test_editor_sticky_scroll.py @@ -0,0 +1,17 @@ +from biscuit.editor.text.sticky_scroll import StickyScrollController, StickyHeaderScope + +def test_sticky_scroll_controller(): + controller = StickyScrollController() + controller.set_scopes([ + StickyHeaderScope(10, "class MyService:"), + StickyHeaderScope(25, " def handle_request(self):"), + StickyHeaderScope(50, " def cleanup(self):"), + ]) + + active = controller.get_active_headers(30) + assert len(active) == 2 + assert active[0] == "class MyService:" + assert "def handle_request" in active[1] + + active_early = controller.get_active_headers(5) + assert len(active_early) == 0 diff --git a/tests/test_editor_types.py b/tests/test_editor_types.py index 778bbfb9..66ec04db 100644 --- a/tests/test_editor_types.py +++ b/tests/test_editor_types.py @@ -153,3 +153,39 @@ def test_get_editor_markdown(self, mock_base): with patch("biscuit.editor.types.is_image", return_value=False): et.get_editor(mock_master, path="test.md") mock_md.assert_called_once() + + +class TestLineNumbersBreakpoints: + def test_toggle_breakpoint_disabled_without_debugger(self): + from biscuit.editor.text.linenumbers import LineNumbers + + ln = LineNumbers.__new__(LineNumbers) + ln.master = MagicMock() + ln.master.debugger = None + ln.breakpoints = set() + ln.redraw = MagicMock() + + ln.toggle_breakpoint(10) + + assert 10 not in ln.breakpoints + ln.redraw.assert_not_called() + ln.master.update_breakpoints.assert_not_called() + + def test_toggle_breakpoint_enabled_with_debugger(self): + from biscuit.editor.text.linenumbers import LineNumbers + + ln = LineNumbers.__new__(LineNumbers) + ln.master = MagicMock() + ln.master.debugger = MagicMock() + ln.breakpoints = set() + ln.redraw = MagicMock() + + ln.toggle_breakpoint(10) + + assert 10 in ln.breakpoints + ln.redraw.assert_called_once() + ln.master.update_breakpoints.assert_called_once_with(ln.breakpoints) + + # toggling again removes it + ln.toggle_breakpoint(10) + assert 10 not in ln.breakpoints diff --git a/tests/test_editor_whitespace.py b/tests/test_editor_whitespace.py new file mode 100644 index 00000000..29d59ca5 --- /dev/null +++ b/tests/test_editor_whitespace.py @@ -0,0 +1,13 @@ +from biscuit.editor.text.whitespace import WhitespaceRenderer + +def test_whitespace_rendering(): + r = WhitespaceRenderer(mode="all") + decorations = r.get_whitespace_decorations(" def\tfoo():\n") + assert len(decorations) == 4 + assert decorations[0][1] == "·" + assert decorations[1][1] == "·" + assert decorations[2][1] == "→" + assert decorations[3][1] == "↵" + + r_none = WhitespaceRenderer(mode="none") + assert len(r_none.get_whitespace_decorations(" ")) == 0 diff --git a/tests/test_editor_word_wrap.py b/tests/test_editor_word_wrap.py new file mode 100644 index 00000000..f355a71c --- /dev/null +++ b/tests/test_editor_word_wrap.py @@ -0,0 +1,15 @@ +from unittest.mock import MagicMock +from biscuit.editor.text.text import Text + +def test_toggle_word_wrap(): + t = MagicMock(spec=Text) + t.cget.return_value = "none" + + res = Text.toggle_word_wrap(t) + assert res == "word" + t.configure.assert_called_with(wrap="word") + + t.cget.return_value = "word" + res2 = Text.toggle_word_wrap(t) + assert res2 == "none" + t.configure.assert_called_with(wrap="none") diff --git a/tests/test_explorer_dnd.py b/tests/test_explorer_dnd.py new file mode 100644 index 00000000..da2973e9 --- /dev/null +++ b/tests/test_explorer_dnd.py @@ -0,0 +1,20 @@ +import os +import tempfile +from biscuit.views.explorer.dnd import FileDragDropController + +def test_file_drag_and_drop(): + ctrl = FileDragDropController() + with tempfile.TemporaryDirectory() as tmpdir: + src = os.path.join(tmpdir, "source.txt") + with open(src, "w") as f: + f.write("content") + target_dir = os.path.join(tmpdir, "dest_dir") + os.makedirs(target_dir) + + ctrl.start_drag(src) + assert ctrl.dragged_path == src + + new_path = ctrl.drop_on(target_dir, copy=True) + assert os.path.exists(new_path) + assert os.path.exists(src) + assert ctrl.dragged_path is None diff --git a/tests/test_explorer_file_nesting.py b/tests/test_explorer_file_nesting.py new file mode 100644 index 00000000..81cdb1b6 --- /dev/null +++ b/tests/test_explorer_file_nesting.py @@ -0,0 +1,8 @@ +from biscuit.views.explorer.nesting import FileNestingController + +def test_file_nesting_patterns(): + ctrl = FileNestingController() + assert ctrl.should_nest_under("app.js", "app.ts") is True + assert ctrl.should_nest_under("app.d.ts", "app.ts") is True + assert ctrl.should_nest_under("server.pyc", "server.py") is True + assert ctrl.should_nest_under("other.js", "app.ts") is False diff --git a/tests/test_explorer_multi_root.py b/tests/test_explorer_multi_root.py new file mode 100644 index 00000000..ecd2da9c --- /dev/null +++ b/tests/test_explorer_multi_root.py @@ -0,0 +1,13 @@ +from biscuit.views.explorer.multi_root import MultiRootWorkspaceManager + +def test_multi_root_workspace(): + mgr = MultiRootWorkspaceManager() + f1 = mgr.add_folder("Backend", "/projects/backend") + f2 = mgr.add_folder("Frontend", "/projects/frontend") + + assert len(mgr.list_folders()) == 2 + assert mgr.list_folders()[0].name == "Backend" + + removed = mgr.remove_folder("/projects/backend") + assert removed == f1 + assert len(mgr.list_folders()) == 1 diff --git a/tests/test_explorer_preview_tab.py b/tests/test_explorer_preview_tab.py new file mode 100644 index 00000000..36155c0a --- /dev/null +++ b/tests/test_explorer_preview_tab.py @@ -0,0 +1,10 @@ +from unittest.mock import MagicMock +from biscuit.views.explorer.directorytree import DirectoryTree + +def test_preview_file_single_click(): + dt = MagicMock() + dt.tree.selected_type.return_value = "file" + dt.tree.selected_path.return_value = "/workspace/main.py" + + DirectoryTree.preview_file(dt, None) + dt.base.open_editor.assert_called_once_with("/workspace/main.py", preview=True) diff --git a/tests/test_extension_dependencies.py b/tests/test_extension_dependencies.py new file mode 100644 index 00000000..73df4e9a --- /dev/null +++ b/tests/test_extension_dependencies.py @@ -0,0 +1,206 @@ +import hashlib +import sys +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +import toml + +from biscuit.extensions.dependencies import DependencyManager +from biscuit.extensions.manager import ExtensionManager + + +class TestDependencyManager: + def test_load_manifest_extension_toml(self): + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) + manifest = { + "extension": { + "id": "my_ext", + "name": "My Extension", + "version": "1.0.0", + }, + "dependencies": { + "python": ["requests", "pydantic>=2.0"], + "tools": ["git", "node"], + }, + } + with open(ext_dir / "extension.toml", "w", encoding="utf-8") as f: + toml.dump(manifest, f) + + mgr = DependencyManager() + data = mgr.load_manifest(ext_dir) + assert data["extension"]["name"] == "My Extension" + assert data["dependencies"]["python"] == ["requests", "pydantic>=2.0"] + assert data["dependencies"]["tools"] == ["git", "node"] + + def test_load_manifest_pyproject_fallback(self): + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) + pyproject = { + "tool": { + "poetry": { + "name": "py_ext", + "version": "0.5.0", + "description": "Fallback ext", + "dependencies": { + "python": "^3.10", + "biscuit-editor": "^3.0.0", + "pytest": "*", + "rich": "^13.0.0", + }, + } + } + } + with open(ext_dir / "pyproject.toml", "w", encoding="utf-8") as f: + toml.dump(pyproject, f) + + mgr = DependencyManager() + data = mgr.load_manifest(ext_dir) + assert data["extension"]["name"] == "py_ext" + assert "rich" in data["dependencies"]["python"] + assert "pytest" in data["dependencies"]["python"] + assert "biscuit-editor" not in data["dependencies"]["python"] + assert "python" not in data["dependencies"]["python"] + + def test_get_declared_dependencies_with_requirements(self): + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) + manifest = { + "dependencies": { + "python": ["click"], + "tools": ["docker"], + } + } + with open(ext_dir / "extension.toml", "w", encoding="utf-8") as f: + toml.dump(manifest, f) + + with open(ext_dir / "requirements.txt", "w", encoding="utf-8") as f: + f.write("# comment\nclick\nrequests>=2.0.0\n") + + mgr = DependencyManager() + pkgs, tools = mgr.get_declared_dependencies(ext_dir) + assert "click" in pkgs + assert "requests>=2.0.0" in pkgs + assert len(pkgs) == 2 + assert tools == ["docker"] + + def test_check_tools(self): + mgr = DependencyManager() + with patch("shutil.which") as mock_which: + mock_which.side_effect = lambda t: "/usr/bin/git" if t == "git" else None + missing = mgr.check_tools("test_ext", ["git", "missing_cli_tool"]) + assert missing == ["missing_cli_tool"] + + def test_ensure_dependencies_already_installed_cache(self): + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) + manifest = { + "dependencies": { + "python": ["requests"], + } + } + with open(ext_dir / "extension.toml", "w", encoding="utf-8") as f: + toml.dump(manifest, f) + + deps_dir = ext_dir / ".deps" + deps_dir.mkdir() + env_sig = f"{sys.version_info.major}.{sys.version_info.minor}:requests" + h = hashlib.sha256(env_sig.encode("utf-8")).hexdigest() + with open(deps_dir / ".installed", "w", encoding="utf-8") as f: + f.write(h) + + mgr = DependencyManager() + with patch("subprocess.run") as mock_run: + ok = mgr.ensure_dependencies(ext_dir) + assert ok is True + mock_run.assert_not_called() + + def test_ensure_dependencies_uv_install(self): + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) + manifest = { + "dependencies": { + "python": ["requests"], + "tools": ["python"], + } + } + with open(ext_dir / "extension.toml", "w", encoding="utf-8") as f: + toml.dump(manifest, f) + + mgr = DependencyManager() + mock_res = MagicMock() + mock_res.returncode = 0 + with patch("shutil.which", return_value="C:\\path\\uv.exe"): + with patch("subprocess.run", return_value=mock_res) as mock_run: + ok = mgr.ensure_dependencies(ext_dir) + assert ok is True + mock_run.assert_called_once() + assert (ext_dir / ".deps" / ".installed").exists() + + def test_add_and_remove_from_sys_path(self): + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) + deps_dir = ext_dir / ".deps" + deps_dir.mkdir() + + mgr = DependencyManager() + added = mgr.add_to_sys_path(ext_dir) + assert added == deps_dir.resolve() + assert str(deps_dir.resolve()) in sys.path + + mgr.remove_from_sys_path(ext_dir) + assert str(deps_dir.resolve()) not in sys.path + + def test_clean_dependencies(self): + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) + deps_dir = ext_dir / ".deps" + deps_dir.mkdir() + (deps_dir / "test.txt").write_text("hello", encoding="utf-8") + + mgr = DependencyManager() + assert deps_dir.exists() + assert mgr.clean_dependencies(ext_dir) is True + assert not deps_dir.exists() + + +class TestExtensionManagerDependencyIntegration: + def test_extension_manager_delegation(self, mock_base): + manager = ExtensionManager(mock_base) + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) / "test_ext" + ext_dir.mkdir() + manifest = { + "extension": {"name": "Test"}, + "dependencies": {"python": ["rich"], "tools": ["git"]}, + } + with open(ext_dir / "extension.toml", "w", encoding="utf-8") as f: + toml.dump(manifest, f) + + with patch.object(manager.dependency_manager, "ensure_dependencies", return_value=True) as mock_ensure: + res = manager.ensure_dependencies(ext_dir) + assert res is True + mock_ensure.assert_called_once_with(ext_dir, force=False) + + def test_cli_deps_command(self): + from biscuit.cli.extensions import deps + with tempfile.TemporaryDirectory() as tmpdir: + ext_dir = Path(tmpdir) + manifest = { + "dependencies": { + "python": ["pytest"], + "tools": ["python"], + } + } + with open(ext_dir / "extension.toml", "w", encoding="utf-8") as f: + toml.dump(manifest, f) + + with patch("pathlib.Path.cwd", return_value=ext_dir): + with patch("click.echo") as mock_echo: + cmd_res = deps.callback(name=None, install=False, force=False) + assert cmd_res is None + echoed = [call.args[0] for call in mock_echo.call_args_list] + assert any("Python packages: pytest" in s for s in echoed) + assert any("System tools:" in s for s in echoed) diff --git a/tests/test_extension_recommendations.py b/tests/test_extension_recommendations.py new file mode 100644 index 00000000..fcbf14cd --- /dev/null +++ b/tests/test_extension_recommendations.py @@ -0,0 +1,24 @@ +from unittest.mock import MagicMock +from biscuit.extensions.manager import ExtensionManager + + +def test_extension_recommendations(): + mgr = ExtensionManager.__new__(ExtensionManager) + mgr.recommended_extensions = ["python", "rust"] + mgr.available_extensions = { + "Python": {"submodule": "python", "name": "Python"}, + "Rust": {"submodule": "rust", "name": "Rust"}, + "Random": {"submodule": "random_ext", "name": "Random"}, + } + mgr.fetch_queue = MagicMock() + mgr.base = MagicMock() + + assert mgr.is_recommended("Python", {"submodule": "python"}) is True + assert mgr.is_recommended("Rust", {"submodule": "rust"}) is True + assert mgr.is_recommended("Random", {"submodule": "random_ext"}) is False + + mgr.display_recommended_extensions() + queued = [call[0][0][0] for call in mgr.fetch_queue.put.call_args_list] + assert "Python" in queued + assert "Rust" in queued + assert "Random" not in queued diff --git a/tests/test_extension_updates.py b/tests/test_extension_updates.py new file mode 100644 index 00000000..dcc11941 --- /dev/null +++ b/tests/test_extension_updates.py @@ -0,0 +1,71 @@ +from unittest.mock import MagicMock, patch + +from biscuit.extensions.manager import ExtensionManager +from biscuit.views.extensions.extension import ExtensionGUI + + +class TestExtensionVersionControl: + def test_version_comparison(self): + assert ExtensionManager._compare_versions("1.0.0", "0.9.0") > 0 + assert ExtensionManager._compare_versions("0.1.0", "0.1.0") == 0 + assert ExtensionManager._compare_versions("0.1.0", "0.2.0") < 0 + assert ExtensionManager._compare_versions("v1.2.3", "1.2.0") > 0 + + def test_check_outdated_extensions(self, mock_base): + manager = ExtensionManager(mock_base) + manager.installed = { + "test_ext": {"name": "Test Extension", "version": "0.1.0"}, + "current_ext": {"name": "Current Extension", "version": "1.0.0"}, + } + manager.available_extensions = { + "test_ext": {"name": "Test Extension", "version": "0.2.0"}, + "current_ext": {"name": "Current Extension", "version": "1.0.0"}, + } + + outdated = manager.check_outdated_extensions() + assert "test_ext" in outdated + assert "current_ext" not in outdated + assert outdated["test_ext"]["installed_version"] == "0.1.0" + assert outdated["test_ext"]["available_version"] == "0.2.0" + + def test_extension_gui_outdated(self, mock_base): + mock_results = MagicMock() + mock_results.base = mock_base + manager = mock_base.extensions_manager + manager.installed = { + "my_ext": {"version": "0.1.0"} + } + + data = { + "submodule": "my_ext", + "name": "My Extension", + "author": "author", + "description": "desc", + "version": "0.2.0", + } + + with patch.object(ExtensionGUI, "installed", True): + ext_gui = ExtensionGUI(mock_results, "my_ext", data) + assert ext_gui.installed_version == "0.1.0" + assert ext_gui.outdated is True + assert ext_gui.install.text == "Update" + + def test_update_extension_from_name(self, mock_base): + manager = ExtensionManager(mock_base) + manager.installed = { + "my_ext": {"name": "My Extension", "version": "0.1.0"} + } + manager.available_extensions = { + "my_ext": { + "submodule": "my_ext", + "name": "My Extension", + "author": "author", + "description": "desc", + "version": "0.2.0", + } + } + manager.update_extension = MagicMock(return_value=None) + + res = manager.update_extension_from_name("my_ext") + assert res is True + manager.update_extension.assert_called_once() diff --git a/tests/test_extensions_category_filter.py b/tests/test_extensions_category_filter.py new file mode 100644 index 00000000..82bff3ed --- /dev/null +++ b/tests/test_extensions_category_filter.py @@ -0,0 +1,30 @@ +from biscuit.views.extensions.category_filter import ExtensionCategory, ExtensionCategoryFilter + + +def test_category_filter_all(): + filt = ExtensionCategoryFilter() + items = [ + {"name": "dark-theme", "category": "Themes"}, + {"name": "py-lsp", "category": "Language Servers"}, + ] + assert len(filt.filter_list(items)) == 2 + + +def test_category_filter_specific(): + filt = ExtensionCategoryFilter() + items = [ + {"name": "solarized", "category": "Themes", "tags": ["theme", "syntax"]}, + {"name": "pylsp", "category": "Language Servers", "tags": ["lsp"]}, + {"name": "black", "category": "Formatters", "tags": ["formatter"]}, + {"name": "gemini-helper", "category": "AI", "tags": ["ai", "assistant"]}, + {"name": "python-debugger", "category": "Debuggers", "tags": ["debugger"]}, + ] + + assert len(filt.filter_list(items, ExtensionCategory.THEMES)) == 1 + assert filt.filter_list(items, ExtensionCategory.THEMES)[0]["name"] == "solarized" + + assert len(filt.filter_list(items, ExtensionCategory.AI)) == 1 + assert filt.filter_list(items, ExtensionCategory.AI)[0]["name"] == "gemini-helper" + + assert len(filt.filter_list(items, ExtensionCategory.DEBUGGERS)) == 1 + assert filt.filter_list(items, ExtensionCategory.DEBUGGERS)[0]["name"] == "python-debugger" diff --git a/tests/test_extensions_installed_tab.py b/tests/test_extensions_installed_tab.py new file mode 100644 index 00000000..2742705e --- /dev/null +++ b/tests/test_extensions_installed_tab.py @@ -0,0 +1,21 @@ +from unittest.mock import MagicMock, patch +import pytest + +from biscuit.views.extensions.installed import Installed + + +def test_installed_tab_methods(): + with patch.object(Installed, "__init__", lambda self, master, *a, **kw: None): + installed = Installed(None) + installed.title = "Installed" + installed.extension_list = MagicMock() + installed.extension_list.items = [] + + item = MagicMock() + installed.add_extension(item) + installed.extension_list.add.assert_called_once_with(item, fill="x") + + installed.extension_list.items.append(item) + installed.clear() + assert len(installed.extension_list.items) == 0 + item.destroy.assert_called_once() diff --git a/tests/test_file_icons.py b/tests/test_file_icons.py new file mode 100644 index 00000000..4d2883cc --- /dev/null +++ b/tests/test_file_icons.py @@ -0,0 +1,15 @@ +from biscuit.common.icons import Icons +from biscuit.views.explorer.directorytree import get_file_icon + + +def test_get_file_icon(): + assert get_file_icon("main.py") == Icons.FILE_CODE + assert get_file_icon("app.ts") == Icons.FILE_CODE + assert get_file_icon("index.html") == Icons.FILE_CODE + assert get_file_icon("data.json") == Icons.JSON + assert get_file_icon("README.md") == Icons.MARKDOWN + assert get_file_icon("logo.png") == Icons.FILE_MEDIA + assert get_file_icon("manual.pdf") == Icons.FILE_PDF + assert get_file_icon("archive.zip") == Icons.FILE_ZIP + assert get_file_icon("binary.exe") == Icons.FILE_BINARY + assert get_file_icon("unknown.xyz123") == Icons.FILE diff --git a/tests/test_find_within_selection.py b/tests/test_find_within_selection.py new file mode 100644 index 00000000..67a1629a --- /dev/null +++ b/tests/test_find_within_selection.py @@ -0,0 +1,15 @@ +from unittest.mock import MagicMock +from biscuit.editor.findreplace.find_replace import FindReplace + +def test_find_within_selection_toggle(): + fr = MagicMock() + fr.base.theme.biscuit = "#ff0000" + fr.find_in_selection = False + + val = FindReplace.toggle_find_in_selection(fr) + assert val is True + assert fr.find_in_selection is True + + val2 = FindReplace.toggle_find_in_selection(fr) + assert val2 is False + assert fr.find_in_selection is False diff --git a/tests/test_git_blame.py b/tests/test_git_blame.py new file mode 100644 index 00000000..7b3414d9 --- /dev/null +++ b/tests/test_git_blame.py @@ -0,0 +1,8 @@ +from biscuit.git.blame import GitBlameController, BlameInfo + +def test_git_blame_annotation(): + info = BlameInfo("John Doe", "2 days ago", "abc1234", "fix: resolve NPE") + formatted = info.format_annotation() + assert "John Doe" in formatted + assert "2 days ago" in formatted + assert "fix: resolve NPE" in formatted diff --git a/tests/test_git_branch_palette.py b/tests/test_git_branch_palette.py new file mode 100644 index 00000000..ecee1fd5 --- /dev/null +++ b/tests/test_git_branch_palette.py @@ -0,0 +1,16 @@ +from unittest.mock import MagicMock +from biscuit.git.git import Git + +def test_branch_switcher_commit_times(): + g = MagicMock(spec=Git) + g.branches = [("main", "2026-10-14 12:00"), ("feat-1", "2026-10-14 11:30")] + g.actionset = MagicMock() + g.repo = MagicMock() + + # Verify formatting logic + items = [ + (f"{str(branch)} ({str(commit_time)})", branch) + for branch, commit_time in g.branches + ] + assert items[0][0] == "main (2026-10-14 12:00)" + assert items[1][0] == "feat-1 (2026-10-14 11:30)" diff --git a/tests/test_git_gutter.py b/tests/test_git_gutter.py new file mode 100644 index 00000000..dc0ca6ea --- /dev/null +++ b/tests/test_git_gutter.py @@ -0,0 +1,9 @@ +from biscuit.git.gutter import GitGutterController + +def test_git_gutter_indicators(): + ctrl = GitGutterController() + ctrl.set_line_diffs(added=[1, 2], modified=[5], deleted=[10]) + assert ctrl.get_mark_color(1) == "#28a745" + assert ctrl.get_mark_color(5) == "#007acc" + assert ctrl.get_mark_color(10) == "#d73a49" + assert ctrl.get_mark_color(99) == "" diff --git a/tests/test_git_init_placeholder.py b/tests/test_git_init_placeholder.py new file mode 100644 index 00000000..7aeaed50 --- /dev/null +++ b/tests/test_git_init_placeholder.py @@ -0,0 +1,11 @@ +from unittest.mock import MagicMock, patch +from biscuit.views.sourcecontrol.placeholder import ChangesTreePlaceholder + +def test_init_repo_button(): + placeholder = MagicMock() + placeholder.base.active_directory = "/tmp/testrepo" + + with patch("git.Repo.init") as mock_init: + ChangesTreePlaceholder.init_repo(placeholder) + mock_init.assert_called_once_with("/tmp/testrepo") + placeholder.base.git.check_git.assert_called_once() diff --git a/tests/test_git_issue_comments.py b/tests/test_git_issue_comments.py new file mode 100644 index 00000000..88ae6129 --- /dev/null +++ b/tests/test_git_issue_comments.py @@ -0,0 +1,11 @@ +from unittest.mock import MagicMock +from biscuit.git.issue import IssueViewer + +def test_issue_load_comments(): + issue = MagicMock() + comments = [ + {"user": {"login": "dev1"}, "body": "Confirmed bug in v1.2"}, + ] + IssueViewer.load_comments(issue, comments) + assert len(issue.comments) == 1 + assert issue.comments[0]["body"] == "Confirmed bug in v1.2" diff --git a/tests/test_git_pr_comments.py b/tests/test_git_pr_comments.py new file mode 100644 index 00000000..3a68908d --- /dev/null +++ b/tests/test_git_pr_comments.py @@ -0,0 +1,12 @@ +from unittest.mock import MagicMock +from biscuit.git.pr import PRViewer + +def test_pr_load_comments(): + pr = MagicMock() + comments = [ + {"user": {"login": "alice"}, "body": "LGTM!"}, + {"user": {"login": "bob"}, "body": "Please add test."} + ] + PRViewer.load_comments(pr, comments) + assert len(pr.comments) == 2 + assert pr.comments[0]["user"]["login"] == "alice" diff --git a/tests/test_git_rebase.py b/tests/test_git_rebase.py new file mode 100644 index 00000000..d1db5845 --- /dev/null +++ b/tests/test_git_rebase.py @@ -0,0 +1,14 @@ +from biscuit.git.rebase import GitRebaseController, RebaseTodoItem + +def test_git_interactive_rebase(): + ctrl = GitRebaseController() + commits = [ + RebaseTodoItem("pick", "aaa", "commit 1"), + RebaseTodoItem("pick", "bbb", "commit 2"), + ] + ctrl.set_commits(commits) + ctrl.squash(1) + assert ctrl.items[1].action == "squash" + + ctrl.drop(0) + assert ctrl.items[0].action == "drop" diff --git a/tests/test_git_remotes.py b/tests/test_git_remotes.py new file mode 100644 index 00000000..6ce7996c --- /dev/null +++ b/tests/test_git_remotes.py @@ -0,0 +1,11 @@ +from unittest.mock import MagicMock +from biscuit.git.remote_manager import GitRemoteManager + +def test_git_remote_management(): + mgr = GitRemoteManager() + mgr.add_remote("upstream", "https://github.com/upstream/repo.git") + assert "upstream" in mgr.list_remotes() + assert mgr._remotes["upstream"] == "https://github.com/upstream/repo.git" + + mgr.remove_remote("upstream") + assert "upstream" not in mgr.list_remotes() diff --git a/tests/test_git_stash.py b/tests/test_git_stash.py new file mode 100644 index 00000000..634f7efc --- /dev/null +++ b/tests/test_git_stash.py @@ -0,0 +1,70 @@ +from unittest.mock import MagicMock +import pytest + +from biscuit.git.repo import GitRepo + + +class TestGitStash: + def test_stash_push_default(self): + repo = GitRepo.__new__(GitRepo) + repo.git = MagicMock() + repo.master = MagicMock() + repo.base = MagicMock() + + repo.stash() + + repo.git.stash.assert_called_once_with("push") + repo.master.update_repo_info.assert_called_once() + + def test_stash_push_with_message_and_keep_index(self): + repo = GitRepo.__new__(GitRepo) + repo.git = MagicMock() + repo.master = MagicMock() + repo.base = MagicMock() + + repo.stash(message="WIP test", keep_index=True) + + repo.git.stash.assert_called_once_with("push", "--keep-index", "-m", "WIP test") + repo.master.update_repo_info.assert_called_once() + + def test_stash_pop(self): + repo = GitRepo.__new__(GitRepo) + repo.git = MagicMock() + repo.master = MagicMock() + repo.base = MagicMock() + + repo.stash_pop(1) + + repo.git.stash.assert_called_once_with("pop", "stash@{1}") + repo.master.update_repo_info.assert_called_once() + + def test_stash_apply(self): + repo = GitRepo.__new__(GitRepo) + repo.git = MagicMock() + repo.master = MagicMock() + repo.base = MagicMock() + + repo.stash_apply(0) + + repo.git.stash.assert_called_once_with("apply", "stash@{0}") + repo.master.update_repo_info.assert_called_once() + + def test_stash_drop(self): + repo = GitRepo.__new__(GitRepo) + repo.git = MagicMock() + repo.master = MagicMock() + repo.base = MagicMock() + + repo.stash_drop(2) + + repo.git.stash.assert_called_once_with("drop", "stash@{2}") + repo.master.update_repo_info.assert_called_once() + + def test_stash_list(self): + repo = GitRepo.__new__(GitRepo) + repo.git = MagicMock() + repo.git.stash.return_value = "stash@{0}: WIP on main\nstash@{1}: WIP on feat\n" + + res = repo.stash_list() + + assert res == ["stash@{0}: WIP on main", "stash@{1}: WIP on feat"] diff --git a/tests/test_git_tags.py b/tests/test_git_tags.py new file mode 100644 index 00000000..9a6a80e0 --- /dev/null +++ b/tests/test_git_tags.py @@ -0,0 +1,7 @@ +from unittest.mock import MagicMock +from biscuit.git.tag_manager import GitTagManager + +def test_git_tags(): + mgr = GitTagManager() + mgr.create_tag("v1.0.0", "Release 1.0.0") + assert "v1.0.0" in mgr.list_tags() diff --git a/tests/test_gitgraph.py b/tests/test_gitgraph.py new file mode 100644 index 00000000..0c85a7de --- /dev/null +++ b/tests/test_gitgraph.py @@ -0,0 +1,54 @@ +from unittest.mock import MagicMock + +from biscuit.views.gitgraph import GitGraph + + +class TestGitGraph: + def test_gitgraph_parsing_and_filtering(self): + mock_master = MagicMock() + mock_master.base = MagicMock() + mock_base = mock_master.base + mock_base.git.repo.git.log.return_value = ( + "* 7a45aa9\x00 (HEAD -> main, tag: v1.0)\x00Tomlin7\x002026-10-15\x00Initial commit\n" + "| * 97a24cd\x00 (feat/branch)\x00Author2\x002026-10-14\x00Feature commit\n" + "|/ \n" + ) + mock_base.git.repo.git.show.return_value = "7a45aa9 Initial commit\n 2 files changed, 10 insertions(+)" + + view = GitGraph(mock_master) + assert view.name == "Git Graph" + + # Check parsing + data = view.get_commit_graph_data() + assert len(data) == 3 + assert data[0]["hash"] == "7a45aa9" + assert data[0]["refs"] == "HEAD -> main, tag: v1.0" + assert data[0]["author"] == "Tomlin7" + assert data[0]["message"] == "Initial commit" + assert data[0]["graph"] == "*" + + assert data[1]["hash"] == "97a24cd" + assert data[1]["refs"] == "feat/branch" + assert data[1]["author"] == "Author2" + + # Test filtering + view.commits = data + view.tree.insert = MagicMock() + view.search_entry.get = lambda: "initial" + view.filter_commits() + assert view.tree.insert.call_count == 1 + call_values = view.tree.insert.call_args[1]["values"] + assert call_values[1] == "7a45aa9" + assert call_values[3] == "Initial commit" + + # Test on_select details + view.tree.selection = MagicMock(return_value=["item1"]) + view.tree.item = MagicMock( + return_value=("*", "7a45aa9", "HEAD -> main", "Initial commit", "Tomlin7", "2026-10-15") + ) + view.details_text = MagicMock() + view.on_select() + view.details_text.insert.assert_any_call( + "end", + "Commit: 7a45aa9 (HEAD -> main)\nAuthor: Tomlin7\nDate: 2026-10-15\nSubject: Initial commit\n\n", + ) diff --git a/tests/test_high_contrast_theme.py b/tests/test_high_contrast_theme.py new file mode 100644 index 00000000..0cc6d4e0 --- /dev/null +++ b/tests/test_high_contrast_theme.py @@ -0,0 +1,15 @@ +from biscuit.settings.theme.high_contrast import HighContrastDark, HighContrastLight + + +def test_high_contrast_dark_properties(): + assert HighContrastDark.name == "High Contrast Dark" + assert HighContrastDark.primary_background == "#000000" + assert HighContrastDark.primary_foreground == "#FFFFFF" + assert HighContrastDark.border == "#6FC3DF" + + +def test_high_contrast_light_properties(): + assert HighContrastLight.name == "High Contrast Light" + assert HighContrastLight.primary_background == "#FFFFFF" + assert HighContrastLight.primary_foreground == "#000000" + assert HighContrastLight.border == "#0F4A85" diff --git a/tests/test_history_navigation.py b/tests/test_history_navigation.py new file mode 100644 index 00000000..25b2ecbe --- /dev/null +++ b/tests/test_history_navigation.py @@ -0,0 +1,61 @@ +from unittest.mock import MagicMock +import pytest + +from biscuit.editor.breadcrumbs.history_navigation import HistoryNavigation + + +class TestHistoryNavigation: + def test_navigation_backward_and_forward(self): + hn = HistoryNavigation.__new__(HistoryNavigation) + hn.base = MagicMock() + hn.left = MagicMock() + hn.right = MagicMock() + + editor = MagicMock() + content = MagicMock() + content.cursor_history = ["1.0", "5.10", "12.0"] + content.cursor_history_index = 2 + content.text = MagicMock() + editor.content = content + hn.base.editorsmanager.active_editor = editor + + # Navigate backward + hn.go_down() + assert content.cursor_history_index == 1 + content.text.mark_set.assert_called_with("insert", "5.10") + + # Navigate backward again + hn.go_down() + assert content.cursor_history_index == 0 + content.text.mark_set.assert_called_with("insert", "1.0") + + # At start, further backward does nothing + hn.go_down() + assert content.cursor_history_index == 0 + + # Navigate forward + hn.go_up() + assert content.cursor_history_index == 1 + content.text.mark_set.assert_called_with("insert", "5.10") + + def test_record_cursor_position(self): + from biscuit.editor.text.editor import TextEditor + + te = TextEditor.__new__(TextEditor) + te.cursor_history = [] + te.cursor_history_index = -1 + te.text = MagicMock() + te.text.index.return_value = "1.0" + + te.record_cursor_position() + assert te.cursor_history == ["1.0"] + assert te.cursor_history_index == 0 + + # Same position is not duplicated + te.record_cursor_position("1.0") + assert len(te.cursor_history) == 1 + + # New position is appended + te.record_cursor_position("5.0") + assert te.cursor_history == ["1.0", "5.0"] + assert te.cursor_history_index == 1 diff --git a/tests/test_hover.py b/tests/test_hover.py new file mode 100644 index 00000000..6b650964 --- /dev/null +++ b/tests/test_hover.py @@ -0,0 +1,17 @@ +from unittest.mock import MagicMock +from biscuit.editor.hover.hover import Hover + + +def test_hover_calculate_size(mock_base): + hover = Hover.__new__(Hover) + hover.base = mock_base + + short_doc = "Short doc" + w, h = hover.calculate_size(short_doc) + assert 220 <= w <= 550 + assert 50 <= h <= 220 + + long_doc = "A very long line that spans many many characters to test wider popup sizing\n" * 15 + w2, h2 = hover.calculate_size(long_doc) + assert w2 > w + assert h2 > h diff --git a/tests/test_installation_compat.py b/tests/test_installation_compat.py new file mode 100644 index 00000000..a4a01739 --- /dev/null +++ b/tests/test_installation_compat.py @@ -0,0 +1,15 @@ +import sys +import pytest + + +def test_cgi_or_legacy_cgi_available(): + """Ensure cgi module is importable (via standard library on <3.13 or legacy-cgi on >=3.13).""" + try: + import cgi + assert cgi is not None + except ImportError: + # If legacy-cgi is not yet installed in current env, verify Python version logic + if sys.version_info < (3, 13): + pytest.fail("cgi standard module should be available on Python < 3.13") + else: + pytest.skip("legacy-cgi not pre-installed in test environment") diff --git a/tests/test_keyboard_shortcuts.py b/tests/test_keyboard_shortcuts.py new file mode 100644 index 00000000..544f0acd --- /dev/null +++ b/tests/test_keyboard_shortcuts.py @@ -0,0 +1,34 @@ +import tkinter as tk +from unittest.mock import MagicMock + +from biscuit.layout.menubar.item import MenubarItem +from biscuit.layout.menubar.menubar import Menubar + + +def test_menubar_item_underline(mock_base): + container = MagicMock() + container.base = mock_base + item = MenubarItem.__new__(MenubarItem) + item.base = mock_base + item.master = container + item.name = "File" + item.underline = 0 + assert item.underline == 0 + + +def test_menubar_bind_accelerators(mock_base): + mb = Menubar.__new__(Menubar) + mb.base = mock_base + item_f = MagicMock() + item_f.name = "File" + item_e = MagicMock() + item_e.name = "Edit" + mb.menus = [item_f, item_e] + + mb.bind_accelerators() + # Check that bind was called for alt shortcuts on base + bound_events = [call[0][0] for call in mock_base.bind.call_args_list] + assert "" in bound_events + assert "" in bound_events + assert "" in bound_events + assert "" in bound_events diff --git a/tests/test_layout_tabscroll.py b/tests/test_layout_tabscroll.py new file mode 100644 index 00000000..b60bb4b2 --- /dev/null +++ b/tests/test_layout_tabscroll.py @@ -0,0 +1,12 @@ +from unittest.mock import MagicMock +from biscuit.layout.editors.tabscroll import TabScroll + +def test_tabscroll_navigation(): + ts = MagicMock(spec=TabScroll) + ts.master = MagicMock() + + TabScroll.scroll_left(ts) + ts.master.tabs.xview_scroll.assert_called_with(1, "units") + + TabScroll.scroll_right(ts) + ts.master.tabs.xview_scroll.assert_called_with(-1, "units") diff --git a/tests/test_lsp_code_actions.py b/tests/test_lsp_code_actions.py new file mode 100644 index 00000000..0a5c9c0c --- /dev/null +++ b/tests/test_lsp_code_actions.py @@ -0,0 +1,15 @@ +from biscuit.language.code_actions import CodeActionManager, CodeAction + +def test_code_actions(): + mgr = CodeActionManager() + mgr.set_actions([ + CodeAction("Import os", "quickfix"), + CodeAction("Extract function", "refactor.extract"), + ]) + qf = mgr.get_quick_fixes() + assert len(qf) == 1 + assert qf[0].title == "Import os" + + rf = mgr.get_refactorings() + assert len(rf) == 1 + assert rf[0].title == "Extract function" diff --git a/tests/test_lsp_configurable_options.py b/tests/test_lsp_configurable_options.py new file mode 100644 index 00000000..bb3267f4 --- /dev/null +++ b/tests/test_lsp_configurable_options.py @@ -0,0 +1,13 @@ +from unittest.mock import MagicMock +from biscuit.language.client import LangServerClient + +def test_configurable_lsp_settings(): + master = MagicMock() + master.base.config.get_nested.return_value = 25000 + tab = MagicMock() + tab.language = "python" + tab.language_alias = "py" + + # Verify delay logic + delay = master.base.config.get_nested("lsp.deletion_delay", 10000) + assert delay == 25000 diff --git a/tests/test_lsp_document_symbols.py b/tests/test_lsp_document_symbols.py new file mode 100644 index 00000000..72b58ef5 --- /dev/null +++ b/tests/test_lsp_document_symbols.py @@ -0,0 +1,25 @@ +from unittest.mock import MagicMock +from biscuit.language.manager import LanguageServerManager + +class FakeDocumentSymbol: + def __init__(self, name, start_line, children=None): + self.name = name + self.range = MagicMock() + self.range.start = MagicMock() + self.range.start.line = start_line + self.range.start.character = 0 + self.children = children or [] + +def test_document_symbol_hierarchy(): + base = MagicMock() + mgr = LanguageServerManager(base) + + child = FakeDocumentSymbol("inner_func", 10) + parent = FakeDocumentSymbol("MyClass", 2, [child]) + + mgr._update_symbols(None, [parent]) + base.settings.symbols_actionset.update.assert_called_once() + symbols = base.settings.symbols_actionset.update.call_args[0][0] + names = [s[0] for s in symbols] + assert "MyClass" in names + assert "inner_func" in names diff --git a/tests/test_lsp_formatting.py b/tests/test_lsp_formatting.py new file mode 100644 index 00000000..62bc3ce6 --- /dev/null +++ b/tests/test_lsp_formatting.py @@ -0,0 +1,7 @@ +from biscuit.language.formatter import DocumentFormatter + +def test_document_formatter(): + fmt = DocumentFormatter(format_on_save=True) + assert fmt.format_on_save is True + res = fmt.apply_edits("a=1", [{"newText": "a = 1\n"}]) + assert res == "a = 1\n" diff --git a/tests/test_lsp_incremental_sync.py b/tests/test_lsp_incremental_sync.py new file mode 100644 index 00000000..b44c1c78 --- /dev/null +++ b/tests/test_lsp_incremental_sync.py @@ -0,0 +1,10 @@ +from biscuit.language.sync import compute_incremental_changes + +def test_incremental_text_sync(): + old = "def foo():\n pass\n" + new = "def foo():\n return 42\n" + changes = compute_incremental_changes(old, new) + assert len(changes) >= 1 + lsp_dict = changes[0].to_lsp_dict() + assert "range" in lsp_dict + assert "text" in lsp_dict diff --git a/tests/test_lsp_rename.py b/tests/test_lsp_rename.py new file mode 100644 index 00000000..0ad9312b --- /dev/null +++ b/tests/test_lsp_rename.py @@ -0,0 +1,13 @@ +from biscuit.language.rename import WorkspaceRenameManager + +def test_workspace_rename(): + mgr = WorkspaceRenameManager() + edit_payload = { + "changes": { + "file:///path/a.py": [{"range": {"start": {"line": 1, "character": 4}}, "newText": "new_name"}], + "file:///path/b.py": [{"range": {"start": {"line": 5, "character": 8}}, "newText": "new_name"}] + } + } + count = mgr.parse_workspace_edit(edit_payload) + assert count == 2 + assert len(mgr.pending_edits) == 2 diff --git a/tests/test_lsp_semantic_tokens.py b/tests/test_lsp_semantic_tokens.py new file mode 100644 index 00000000..9bd658a8 --- /dev/null +++ b/tests/test_lsp_semantic_tokens.py @@ -0,0 +1,16 @@ +from biscuit.language.semantic_tokens import SemanticTokensDecoder + +def test_semantic_tokens_decoder(): + decoder = SemanticTokensDecoder() + # 5 integers per token: deltaLine, deltaStart, length, tokenType, tokenModifiers + raw_data = [2, 5, 8, 1, 0, 0, 10, 4, 10, 0] + tokens = decoder.decode_raw_tokens(raw_data) + assert len(tokens) == 2 + assert tokens[0].line == 2 + assert tokens[0].start_char == 5 + assert tokens[0].length == 8 + assert tokens[0].token_type == "class" + + assert tokens[1].line == 2 + assert tokens[1].start_char == 15 + assert tokens[1].token_type == "function" diff --git a/tests/test_lsp_signature_help.py b/tests/test_lsp_signature_help.py new file mode 100644 index 00000000..be5952d9 --- /dev/null +++ b/tests/test_lsp_signature_help.py @@ -0,0 +1,17 @@ +from biscuit.language.signature_help import SignatureHelpController, SignatureInfo, ParameterInfo + +def test_signature_help(): + ctrl = SignatureHelpController() + sig = SignatureInfo( + label="add(a: int, b: int) -> int", + parameters=[ParameterInfo("a: int"), ParameterInfo("b: int")], + ) + ctrl.update_signature(sig) + assert ctrl.is_visible + assert len(ctrl.active_signature.parameters) == 2 + + assert ctrl.compute_active_parameter("add(10, ") == 1 + assert ctrl.compute_active_parameter("add(10, 20, ") == 2 + + ctrl.dismiss() + assert not ctrl.is_visible diff --git a/tests/test_palette_secondary_text.py b/tests/test_palette_secondary_text.py new file mode 100644 index 00000000..e618708b --- /dev/null +++ b/tests/test_palette_secondary_text.py @@ -0,0 +1,13 @@ +from unittest.mock import MagicMock +from biscuit.common.palette.item import PaletteItem + +def test_palette_item_secondary_and_shortcut(): + item = MagicMock() + item.text = "File: Open File" + item.description = "Open existing file from disk" + item.shortcut = "Ctrl+O" + + display = PaletteItem.get_full_display_text(item) + assert "File: Open File" in display + assert "Open existing file from disk" in display + assert "[Ctrl+O]" in display diff --git a/tests/test_search_exclude_patterns.py b/tests/test_search_exclude_patterns.py new file mode 100644 index 00000000..811b73ce --- /dev/null +++ b/tests/test_search_exclude_patterns.py @@ -0,0 +1,12 @@ +from unittest.mock import MagicMock +from biscuit.views.search.search import Search + +def test_search_exclude_box(): + s = MagicMock() + s.exclude_box = MagicMock() + s.exclude_box.get.return_value = "*.log, node_modules" + + excludes = [e.strip() for e in s.exclude_box.get().split(",") if e.strip()] + assert len(excludes) == 2 + assert "*.log" in excludes + assert "node_modules" in excludes diff --git a/tests/test_search_refresh_editors.py b/tests/test_search_refresh_editors.py new file mode 100644 index 00000000..f8e27923 --- /dev/null +++ b/tests/test_search_refresh_editors.py @@ -0,0 +1,11 @@ +from unittest.mock import MagicMock +from biscuit.views.search.results import Results + +def test_refresh_open_editors_after_replace(): + sr = MagicMock() + ed1 = MagicMock() + ed1.path = "/path/to/file.py" + sr.base.editorsmanager.active_editors = [ed1] + + Results.refresh_open_editors(sr, "/path/to/file.py") + ed1.reload.assert_called_once() diff --git a/tests/test_settings.py b/tests/test_settings.py index 9ed4e45c..49deabbd 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -133,3 +133,25 @@ def test_format_with_none(self): f = Formattable("clone {}") result = f.format(None) assert result == "clone None" + + +class TestSettingsFontSafety: + def test_setup_font_handles_extrafont_failure(self): + from biscuit.settings.settings import Settings + + base = MagicMock() + base.call.return_value = None + base._w = ".mock" + base.game_manager.get_games.return_value = [] + base.parentdir = os.path.dirname(os.path.dirname(__file__)) + + with patch("biscuit.settings.settings.extra", None): + s = Settings.__new__(Settings) + s.base = base + s.config = MagicMock() + s.config.font = ("Fira Code", 12) + s.config.uifont = ("Fira Code", 10) + s.resources = MagicMock() + s.setup_font() + assert s.iconfont is None + diff --git a/tests/test_settings_font.py b/tests/test_settings_font.py new file mode 100644 index 00000000..835925ae --- /dev/null +++ b/tests/test_settings_font.py @@ -0,0 +1,14 @@ +from biscuit.settings.font import FontManager, DEFAULT_FALLBACK_FONTS + + +def test_font_manager_fallbacks(): + fonts = FontManager.get_system_font_families() + assert len(fonts) > 0 + assert any("Consolas" in f or "Courier" in f or "Mono" in f for f in fonts) + + +def test_font_availability(): + custom_list = ["Fira Code", "Consolas", "CustomFont"] + assert FontManager.is_font_available("Fira Code", custom_list) is True + assert FontManager.is_font_available("fira code", custom_list) is True + assert FontManager.is_font_available("NonExistentFont", custom_list) is False diff --git a/tests/test_settings_list_item.py b/tests/test_settings_list_item.py new file mode 100644 index 00000000..d3d99344 --- /dev/null +++ b/tests/test_settings_list_item.py @@ -0,0 +1,23 @@ +from unittest.mock import MagicMock, patch +from biscuit.settings.editor.items import ListItem + + +def test_list_item_add_and_remove(): + with patch.object(ListItem, "__init__", lambda self, *a, **kw: None): + item = ListItem(None) + item.value = ["*.pyc", "*.git"] + item.add_entry_var = MagicMock() + item.callback = MagicMock() + item.render_rows = MagicMock() + item.change = lambda: item.callback(item.value) if item.callback else None + + # Add item + item.add_item("node_modules") + assert "node_modules" in item.value + assert len(item.value) == 3 + item.callback.assert_called_with(["*.pyc", "*.git", "node_modules"]) + + # Remove item at index 0 + item.remove_item(0) + assert item.value == ["*.git", "node_modules"] + item.callback.assert_called_with(["*.git", "node_modules"]) diff --git a/tests/test_settings_search_filter.py b/tests/test_settings_search_filter.py new file mode 100644 index 00000000..0479d712 --- /dev/null +++ b/tests/test_settings_search_filter.py @@ -0,0 +1,25 @@ +from types import SimpleNamespace +from biscuit.settings.editor.search_filter import SettingsSearchFilter + + +def test_search_filter_matches(): + assert SettingsSearchFilter.matches("Font Size", "Editor font size in pixels", "font") is True + assert SettingsSearchFilter.matches("Font Size", "Editor font size in pixels", "pixels") is True + assert SettingsSearchFilter.matches("Color Theme", "UI theme", "size") is False + assert SettingsSearchFilter.matches("Color Theme", None, "") is True + + +def test_search_filter_items(): + items = [ + SimpleNamespace(name="Font Size", description="Editor font size"), + SimpleNamespace(name="Tab Size", description="Number of spaces per tab"), + SimpleNamespace(name="Word Wrap", description="Wrap long lines"), + ] + + filtered = SettingsSearchFilter.filter_items(items, "size") + assert len(filtered) == 2 + assert {i.name for i in filtered} == {"Font Size", "Tab Size"} + + filtered_wrap = SettingsSearchFilter.filter_items(items, "wrap") + assert len(filtered_wrap) == 1 + assert filtered_wrap[0].name == "Word Wrap" diff --git a/tests/test_statusbar_zoom.py b/tests/test_statusbar_zoom.py new file mode 100644 index 00000000..77e76100 --- /dev/null +++ b/tests/test_statusbar_zoom.py @@ -0,0 +1,35 @@ +from unittest.mock import MagicMock +import pytest + +from biscuit.layout.statusbar.statusbar import Statusbar + + +class TestStatusbarZoom: + def test_change_font_size(self): + sb = Statusbar.__new__(Statusbar) + sb.base = MagicMock() + sb.font_size_btn = MagicMock() + + sb.change_font_size(16) + + sb.base.settings.font.configure.assert_called_once_with(size=16) + sb.base.editorsmanager.active_editor.content.set_fontsize.assert_called_once_with(16) + sb.font_size_btn.set_text.assert_called_once_with("16pt") + + def test_change_custom_font_size(self): + sb = Statusbar.__new__(Statusbar) + sb.base = MagicMock() + sb.font_size_btn = MagicMock() + + sb.change_custom_font_size(" 18 ") + + sb.base.settings.font.configure.assert_called_once_with(size=18) + sb.font_size_btn.set_text.assert_called_once_with("18pt") + + def test_change_font_size_invalid(self): + sb = Statusbar.__new__(Statusbar) + sb.base = MagicMock() + sb.font_size_btn = MagicMock() + + sb.change_font_size("not-a-number") + sb.font_size_btn.set_text.assert_not_called() diff --git a/tests/test_tab_reordering.py b/tests/test_tab_reordering.py new file mode 100644 index 00000000..7ef3fcde --- /dev/null +++ b/tests/test_tab_reordering.py @@ -0,0 +1,23 @@ +from unittest.mock import MagicMock +from biscuit.layout.editors.pane import EditorPane + + +def test_pane_tab_reorder_and_scroll(mock_base): + pane = EditorPane.__new__(EditorPane) + pane.base = mock_base + pane.tab_container = MagicMock() + + t1, t2, t3 = MagicMock(), MagicMock(), MagicMock() + pane.active_tabs = [t1, t2, t3] + + # Reorder t1 from 0 to 2 + pane.reorder_tab(t1, 2) + assert pane.active_tabs == [t2, t3, t1] + + # Scroll left + pane.scroll_tabs_left() + assert pane.active_tabs == [t3, t1, t2] + + # Scroll right + pane.scroll_tabs_right() + assert pane.active_tabs == [t2, t3, t1] diff --git a/tests/test_terminal_command_queue.py b/tests/test_terminal_command_queue.py new file mode 100644 index 00000000..fd408e08 --- /dev/null +++ b/tests/test_terminal_command_queue.py @@ -0,0 +1,28 @@ +from biscuit.views.terminal.queue import TerminalCommandQueue + + +def test_command_queue_execution(): + executed = [] + queue = TerminalCommandQueue(executor=lambda cmd: executed.append(cmd)) + + queue.put("echo hello") + queue.put("ls -la") + + assert executed == ["echo hello", "ls -la"] + assert queue.pending_count == 0 + + +def test_command_queue_buffering(): + executed = [] + queue = TerminalCommandQueue(executor=lambda cmd: executed.append(cmd)) + queue.set_ready(False) + + queue.put("git status") + queue.put("git log") + + assert len(executed) == 0 + assert queue.pending_count == 2 + + queue.set_ready(True) + assert executed == ["git status", "git log"] + assert queue.pending_count == 0 diff --git a/tests/test_terminal_custom_shells.py b/tests/test_terminal_custom_shells.py new file mode 100644 index 00000000..7bbd33fd --- /dev/null +++ b/tests/test_terminal_custom_shells.py @@ -0,0 +1,41 @@ +from unittest.mock import MagicMock +import pytest + +from biscuit.views.terminal.shells import create_custom_shell_class, CustomShell +from biscuit.views.terminal.terminal import Terminal + + +class TestCustomShells: + def test_create_custom_shell_class(self): + cls = create_custom_shell_class("NuShell", "/usr/bin/nu") + assert cls.name == "NuShell" + assert cls.shell == "/usr/bin/nu" + assert issubclass(cls, CustomShell) + + def test_refresh_shells_menu_loads_configured_shells(self): + term = Terminal.__new__(Terminal) + term.base = MagicMock() + term.base.config.get_nested.return_value = { + "Zsh": "/bin/zsh", + "Fish": "/usr/bin/fish" + } + term.addmenu = MagicMock() + term.open_shell = MagicMock() + + term.refresh_shells_menu() + + term.addmenu.clear.assert_called_once() + added_commands = [call.args[0] for call in term.addmenu.add_command.call_args_list] + assert "Zsh" in added_commands + assert "Fish" in added_commands + + def test_open_custom_shell(self): + term = Terminal.__new__(Terminal) + term.base = MagicMock() + term.open_shell = MagicMock() + + term.open_custom_shell("/bin/zsh", "Zsh") + term.open_shell.assert_called_once() + shell_cls = term.open_shell.call_args[0][0] + assert shell_cls.name == "Zsh" + assert shell_cls.shell == "/bin/zsh" diff --git a/tests/test_theme_switching.py b/tests/test_theme_switching.py new file mode 100644 index 00000000..2bbf3604 --- /dev/null +++ b/tests/test_theme_switching.py @@ -0,0 +1,44 @@ +from unittest.mock import MagicMock +from biscuit.settings.config import Config +from biscuit.api.theme import Theme as APITheme + + +class CustomTheme: + def __init__(self): + self.name = "Custom" + + +def test_config_theme_registration_and_switching(mock_base): + cfg = Config.__new__(Config) + cfg.base = mock_base + cfg.data = {} + cfg.registered_themes = {} + + # Register custom theme + cfg.register_theme("my_theme", CustomTheme) + assert "my_theme" in cfg.registered_themes + + # Switch to registered theme + cfg.set_theme("my_theme") + assert isinstance(cfg.theme, CustomTheme) + assert cfg.data["theme"] == "my_theme" + + # Switch to built-in light theme + cfg.set_theme("light") + assert "VSCodeLight" in cfg.theme.__class__.__name__ + + # Switch to built-in dark theme + cfg.set_theme("dark") + assert "VSCodeDark" in cfg.theme.__class__.__name__ + + +def test_api_theme_registration(mock_base): + mock_base.settings = MagicMock() + mock_base.settings.config = MagicMock() + + api_theme = APITheme(mock_base) + api_theme.register_theme("custom", CustomTheme) + mock_base.settings.config.register_theme.assert_called_once_with("custom", CustomTheme) + + api_theme.set_theme("custom") + mock_base.settings.config.set_theme.assert_called_once_with("custom") diff --git a/uv.lock b/uv.lock index ec7d93b0..fdf68b28 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,9 @@ revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", ] [[package]] @@ -17,7 +19,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.1" +version = "3.14.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -30,126 +32,168 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/67/58ded4b3f2e10f94972d8928050c85330e249a31dd45a0e5f3c0e9c3fa05/aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e", size = 766140, upload-time = "2026-06-07T21:05:37.471Z" }, - { url = "https://files.pythonhosted.org/packages/18/68/4ae5b4e08943f316594bb68da89957d3baf5760588fa09509594bd777e4b/aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491", size = 519430, upload-time = "2026-06-07T21:05:40.751Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c1/316c8f3549dbe5245f92bfd523ec6f32dd4d98cafe21df3f6a19b1184c75/aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce", size = 514406, upload-time = "2026-06-07T21:05:42.111Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ee/fb0ac28684e8d753b83c8a4eebc19a5846912aa0a4daaabb6a9936363840/aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3", size = 1703649, upload-time = "2026-06-07T21:05:43.427Z" }, - { url = "https://files.pythonhosted.org/packages/3b/57/aa2beab673331f111885db8a7b69dfe3ab0e53e446a0ace18ca694b4dc58/aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505", size = 1675126, upload-time = "2026-06-07T21:05:44.897Z" }, - { url = "https://files.pythonhosted.org/packages/47/ea/dad128abe365e79be03b16ed464198ac73e0d257e8260c6f7d6f31cbef26/aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521", size = 1771558, upload-time = "2026-06-07T21:05:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/63/f3/b5b4e10327cb85d34d24232c6b71b64602f190b3ccb238a043ac6b187dac/aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd", size = 1856631, upload-time = "2026-06-07T21:05:47.844Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/93294c3045775c708ac8310eb3d3622a11d2951345ad590d532d62a1faa4/aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb", size = 1714139, upload-time = "2026-06-07T21:05:49.982Z" }, - { url = "https://files.pythonhosted.org/packages/29/c4/93067c85a0373492ce8e577435203c5947c454af074ac48ed4f3a1b9dd4a/aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42", size = 1588321, upload-time = "2026-06-07T21:05:51.431Z" }, - { url = "https://files.pythonhosted.org/packages/c4/39/9ff91aaf02af8b7b8222a987466da539f154c3e01732c22b5f5a20a8ee66/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b", size = 1670375, upload-time = "2026-06-07T21:05:53.109Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e4/77452a3676b8d99ac1375f77691d6bf65ea6e9f4b201b82ef77c916dc767/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192", size = 1690933, upload-time = "2026-06-07T21:05:54.902Z" }, - { url = "https://files.pythonhosted.org/packages/7d/84/b0059a7c7fc05ea23f3bc1596ba91c12f79588b9450564a24cac37536d0a/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05", size = 1740798, upload-time = "2026-06-07T21:05:56.458Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3a/e2a513ecbfc362591caa51a7f7e011b3bfc8938b388ae44cd95560d36999/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe", size = 1576412, upload-time = "2026-06-07T21:05:57.953Z" }, - { url = "https://files.pythonhosted.org/packages/a1/10/08f1654f538f93d36dcac66310a06eefce4641cdafca83f9f0a5317be254/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d", size = 1750199, upload-time = "2026-06-07T21:05:59.488Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/d91b70c57d8b8e9611e4a2e52238ca3698d3dc1c2efe25b7a9bf594ac584/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966", size = 1699356, upload-time = "2026-06-07T21:06:01.131Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f1/15340176f35ff61b95dbe34020bcf43f9e624a2d7bbac934715ff97d2033/aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6", size = 458939, upload-time = "2026-06-07T21:06:02.86Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c2/a2f1ec5b37f903109e43ae2862268cfe4a67a60c1b2cf43169fcdff5995f/aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df", size = 482583, upload-time = "2026-06-07T21:06:04.666Z" }, - { url = "https://files.pythonhosted.org/packages/d0/7a/7b56f6732ef79530afaa72aa335d41b67c8d79b946995f0b11ad72985435/aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c", size = 453470, upload-time = "2026-06-07T21:06:06.322Z" }, - { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" }, - { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" }, - { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" }, - { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" }, - { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" }, - { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" }, - { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" }, - { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" }, - { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" }, - { url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" }, - { url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" }, - { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, - { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, - { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, - { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, - { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, - { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, - { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, - { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, - { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, - { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, - { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, - { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, - { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, - { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, - { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, - { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, - { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, - { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, - { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, - { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, - { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, - { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, - { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, - { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, - { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, - { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, - { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, - { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, - { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, - { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, - { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, - { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, - { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, - { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, - { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, - { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, - { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/93/2f/6a91adaa2dc26877d6ed2f54c0370c8910f019db7d77c5c6a194611e93ea/aiohttp-3.14.4.tar.gz", hash = "sha256:831fc5bd39ec2517851e348f613ddb5447a47cf4b71cb09845af7ad7ed45d8f9", size = 8077839, upload-time = "2026-10-05T17:43:48.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/08/290e4be6e3ed239751c446505c324406a4ae8a82a15009a12a3c550f18d9/aiohttp-3.14.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dea3b3653c1d0d6babd8ce09cb9e5a7776864315d9cfb1c569f926389874a261", size = 834853, upload-time = "2026-10-05T17:37:45.079Z" }, + { url = "https://files.pythonhosted.org/packages/81/81/ed5e04508ba1fcf9a34e0449c78009956e8256f62ebef708e2948594cf94/aiohttp-3.14.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3099a389e5b5900fc79c6f613015ff3f8fa06a519744e6001cc68a31ba418cac", size = 560957, upload-time = "2026-10-05T17:37:47.497Z" }, + { url = "https://files.pythonhosted.org/packages/90/28/1a91411c53704db0481cc64b0f0c9465aa9d738856abdcaa3a5bbbe18180/aiohttp-3.14.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70247156fc76e92919f9aacb820556cb30289a54784299a31714d030bb604d84", size = 553475, upload-time = "2026-10-05T17:37:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2e/1038f3fa8832f9f4bed0f9f751f70dbc52bc4a09c66788f97bbfb0ce3d88/aiohttp-3.14.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdf2d2a6ad402797422949f4a200a60d43e3a021695c8a7804aa1d5bbef22652", size = 1845211, upload-time = "2026-10-05T17:37:50.728Z" }, + { url = "https://files.pythonhosted.org/packages/ea/54/e2253f17604c5c1c69a05e3c12400b51cd6a5fb32d7175592114d8c7a470/aiohttp-3.14.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ba835fad8d4b5eaea0d34f9461cf499737a720a5f8bc6768d2f6d98a1d3ba8b5", size = 1814629, upload-time = "2026-10-05T17:37:52.481Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ef/7bd1e45db925376720555d7268c2d642b113105263d37a522e41e1ba3a6c/aiohttp-3.14.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d4415ca5c9c8b24bc2f5733f870d4eab4929e680203559ba60131dd1a27852a", size = 1898528, upload-time = "2026-10-05T17:37:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/5d/40/26d09668e7e0cadc16027566765aa4ce26e8a41276bad6764848c8b04d11/aiohttp-3.14.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6757e02c2c2b0d7985f57be87cf4a7c5b35708f849e1feaea3f77656ddb9dff", size = 2024385, upload-time = "2026-10-05T17:37:55.626Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/0f1c4aab1cc9518e1bf874e2481e59e541b3e32330b9542e52bb2712f86f/aiohttp-3.14.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a950bd8418ef54eccf5f9f517f08e37c1adab4bc50a470a0422ee1cc1032e29", size = 1859130, upload-time = "2026-10-05T17:37:57.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/98/09d5998ba4b5fe9f0cb97d794fab9d1b3d8245c5ec6a5fb84e41a8cec142/aiohttp-3.14.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34cf6f2fe4fc1d7183e8cc0d8c48bbdda828a740bc372d8278126149714d343c", size = 1714768, upload-time = "2026-10-05T17:37:58.831Z" }, + { url = "https://files.pythonhosted.org/packages/c2/63/39d01a74d525d6443084d47ad4aa8c719f44a030149d03566f3761527465/aiohttp-3.14.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc0fbe4f79c3b156d754dd062718ffe404c3b31ca878d9d2b88ecdbc72ef8369", size = 1813228, upload-time = "2026-10-05T17:38:00.611Z" }, + { url = "https://files.pythonhosted.org/packages/39/6b/183d7b969bdb7892e86af74147b4afedbb4e03b8cad5006e41aa2034c288/aiohttp-3.14.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d3d50a5a07a2b31a4369b86dd4a3acf3c2577c45091dbd5e884b198dec56480b", size = 1831844, upload-time = "2026-10-05T17:38:02.272Z" }, + { url = "https://files.pythonhosted.org/packages/2c/be/4c6df02b62083efd3f008974cf9016a4afd2023f5a9d2c3fd55499fcbac6/aiohttp-3.14.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:de15c46a63f46da881d219e44a04778c40114c756e5575b5ea38143b20eb3713", size = 1868738, upload-time = "2026-10-05T17:38:03.831Z" }, + { url = "https://files.pythonhosted.org/packages/86/21/406be8b0cf1105a35a6a6da16a95796be39b354e29bfca0d1626d822835f/aiohttp-3.14.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:caa4c0fc9b25e8c679baac68337e9c00deb6baf6601fe6f07e8511469cc1ceba", size = 1700006, upload-time = "2026-10-05T17:38:05.631Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e6/beac98020ea0885ea1ee3b2ef59ec952405590fe2ccef2fa4ef7614c3c2b/aiohttp-3.14.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3647f8c090632c70abb1e80826e4ee24faea3f895f0283b60e91a0b17aea5e34", size = 1889997, upload-time = "2026-10-05T17:38:07.578Z" }, + { url = "https://files.pythonhosted.org/packages/c4/89/d608dd9571988a529c7fead5bc79ad36b56fd52bad7941f40774acbc87e9/aiohttp-3.14.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3f3e8030169843fb652a60a3d32c206e03f260198aa398985a48b41cd7e42f0a", size = 1844619, upload-time = "2026-10-05T17:38:10.003Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7d/7f51e6338d2c74b75173ee66fcb50d778d5407c32f34e76ef1c4b25d0d75/aiohttp-3.14.4-cp310-cp310-win32.whl", hash = "sha256:a1205cbdddfca1428129bb6d23e0b292b09e681df951fa828145d83a63ffc0ca", size = 489598, upload-time = "2026-10-05T17:38:11.795Z" }, + { url = "https://files.pythonhosted.org/packages/33/2a/3434e450a4395fc07f3070d6ac24553f0b170b173e949216b1723395028c/aiohttp-3.14.4-cp310-cp310-win_amd64.whl", hash = "sha256:e90980e0454a043f81b435309a089ac5deab883779557eb58d004f33990f61e4", size = 512204, upload-time = "2026-10-05T17:38:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/53/c0/1d89303cf1bea5418268c1cb82b872661bad027e5fd785c2d30d0a31ee6f/aiohttp-3.14.4-cp310-cp310-win_arm64.whl", hash = "sha256:e63eecf9a4b670a24055d5c0486128af16f83b02d9817abd685d3238940f8752", size = 492725, upload-time = "2026-10-05T17:38:14.825Z" }, + { url = "https://files.pythonhosted.org/packages/81/e3/998dc75634ecd2a24f33bab480b57313059d55bcadff8825a46b7c80a8af/aiohttp-3.14.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb0093ca0817019d539f0d27bdf98e823944e05bb7efd9d2337a45b92e4d1b06", size = 832892, upload-time = "2026-10-05T17:38:16.373Z" }, + { url = "https://files.pythonhosted.org/packages/58/c9/9bacc26397854440d169f70ae00ea51721f504611d3a64cf043643792c32/aiohttp-3.14.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f482dc9c8309801b4259dd218609d3dae0a7bcdedeac56d90500eabaa0c23465", size = 559893, upload-time = "2026-10-05T17:38:18.74Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/90c5856014328a8b1e9a753de794edea282182b776ff65f0602ad4ffa520/aiohttp-3.14.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa800bb7c1d00e166493931f2ddacb982db913292503dc2f4884db1f2bc5f105", size = 552850, upload-time = "2026-10-05T17:38:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/9b/88/e71c29ce6daa78fcee0feb1992f0fdc408f09f42c3311b1f1c68c36f16ea/aiohttp-3.14.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d90e4dae71b26597f6d1fd12af7d092ae67551edf0e524b73e0687214b79ee33", size = 1928941, upload-time = "2026-10-05T17:38:22.218Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4f/f0f3bebfa521a2ce5fd4302c937701cbc50cd6d908beaa597388639b0b98/aiohttp-3.14.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b681054b8c3b86aa6a0384b6ad7f5cb78dccbabab1f4a3a6793f98ad1378959", size = 1882092, upload-time = "2026-10-05T17:38:24.016Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0d/5df041fd25dceb4f4cafbbee49bb2c1e2d54a91aedecaf03b77cbb404c44/aiohttp-3.14.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dfe80b41bcf8d80d2125fb99a171adeb86db2fcb35c1a68e0226acba0ba7f03", size = 1986051, upload-time = "2026-10-05T17:38:26.022Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/e95eddafffef7c3a74446f67ac4e477a49cec2c78f2ab6c86d1acfd90d2d/aiohttp-3.14.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:71e4a59c6a8c5a6ae8b0b696c32635913e8f526cc01448762eaf5a3c59df8a4f", size = 2107840, upload-time = "2026-10-05T17:38:28.112Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/1f83990fea249dcb10f48086726953546efc9199c96b6be7114532ac361f/aiohttp-3.14.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f887c3b7fb3058fc1ccd917f52078540c18d938d12018cb9edd64b6811d20c57", size = 1930721, upload-time = "2026-10-05T17:38:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d1/48184a186ea26df4f81dd466144dcab7f9cdd1e131bfee0f0e41278c6f0d/aiohttp-3.14.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0da4525e9ba145a11617d2cd7e44fe1acaf471f9c72a0cad28e72879c1020fbb", size = 1774508, upload-time = "2026-10-05T17:38:31.8Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/a9ec758c6d3850453838c31b56d1e4499eb47c80883753a12dcb203f9afa/aiohttp-3.14.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7e8d3c5caa44fdbee3eba1aa1417aa58c74c95d4089fe3c451911117fcbb2f3", size = 1902311, upload-time = "2026-10-05T17:38:33.791Z" }, + { url = "https://files.pythonhosted.org/packages/d6/49/9a2be54900f2dea6e63bbf9628fced604686a2e301130ccde0b1aa2e7e53/aiohttp-3.14.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:09092cf906c18c824b6b16881da5a89cc9cfa9b15344e496bcf880e9045bee33", size = 1912107, upload-time = "2026-10-05T17:38:35.614Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/151254abfc0fe5cbb8e6c41ad04d52122db0af176fb31dbf3bc44e081bda/aiohttp-3.14.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:dfd144837e92264878bba6d3a7ffa8206668b21e87748740afd45d193e48c87e", size = 1960207, upload-time = "2026-10-05T17:38:37.322Z" }, + { url = "https://files.pythonhosted.org/packages/23/99/92c8dce336ac5784755a3144623528516f2e3b6b4871d02cad5a0642ae66/aiohttp-3.14.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9413cffe4e0d654b9b524f9c99dde8cbdb37e0ed9a2c4edd2e63adbac6b374e8", size = 1760168, upload-time = "2026-10-05T17:38:39.214Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/f05fd8d1a9595730db8d522882eea76429b9be2ed5337609f7c6e3d9d49c/aiohttp-3.14.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f50d719f97ba488dfb8e306cf8c8cc172ff0683ec0af53917b288229aa555ba5", size = 1979946, upload-time = "2026-10-05T17:38:40.927Z" }, + { url = "https://files.pythonhosted.org/packages/b9/14/8941aa73ffceabc2a0eb26878b4f1b2958fb7b5358f97cc49b1c00ed9258/aiohttp-3.14.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:68ed4824d96b7afec1f3a8d7fba190fe282718041c5772e20298003e1e3e5d66", size = 1923375, upload-time = "2026-10-05T17:38:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/18/b5/ef275b63c8bfe0a21e7e8d3ef0e67c4745ed403f1894fe5205057cedd3b9/aiohttp-3.14.4-cp311-cp311-win32.whl", hash = "sha256:d7a41d1427136828e6b11d3f3fd194d4357d5205a5b610be7ae07b1ba52ab974", size = 488414, upload-time = "2026-10-05T17:38:44.978Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0b/21bdd70a1e072222d9dbd96bed4fbed26f7c46c5a20af11b78d32fd049d6/aiohttp-3.14.4-cp311-cp311-win_amd64.whl", hash = "sha256:2efbdb87e79d596325c4eefaef0f4495d701145e882351310ebc538e854dc7a3", size = 512755, upload-time = "2026-10-05T17:38:47.036Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4c/bb324a7d20a9598187bb028ca8d6bcf66c63589c8632420919dd576964ed/aiohttp-3.14.4-cp311-cp311-win_arm64.whl", hash = "sha256:ac6e6f90d9360e460c873f945f11dc8698db0a71fec6612823f1aaaab2c11faa", size = 492960, upload-time = "2026-10-05T17:38:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/22/02/db5935d45347fa8ede56b777930740fb0e72bbb4473ddb6ff8541f6f2afa/aiohttp-3.14.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:55b0b335982b0db7117dff7f6a8e3aad43c4105f9e392ca04f9fc8d958a95cce", size = 817689, upload-time = "2026-10-05T17:38:50.69Z" }, + { url = "https://files.pythonhosted.org/packages/e6/56/f00aa46127e6342f019d567853eeeb932e04fb9b9424b5fb981ad68f4178/aiohttp-3.14.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d0d8e435582b0d53009dac99e26a3d2be932ed7c9dbfcfc70f3fce1cf943bd5", size = 549599, upload-time = "2026-10-05T17:38:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bc/825c09b09962253a2103e4a88c04374ee939c0c8ea31c270daa0308efb07/aiohttp-3.14.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf734016932d68e1324cfb638cac7a0866a83933a33badf15e836a7e24efb8c0", size = 548132, upload-time = "2026-10-05T17:38:54.629Z" }, + { url = "https://files.pythonhosted.org/packages/40/80/8a1bc8036540fe7cb38219cc5c0aa67d32e9068165725e8199ea8200b08b/aiohttp-3.14.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:322e3d741b3133bcbb981a41d4f9b8ac3545c18de49c662c2b1f4e1c770b98ad", size = 1898485, upload-time = "2026-10-05T17:38:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fe/65b464d8520ace0d65b4ca69bc9abe175f842c8ce8d1522d363d9669903f/aiohttp-3.14.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15a3bb5f90a4e515071f3617a45dfe26560beb834a6b6e108cdf4bc9e9719ec3", size = 1875334, upload-time = "2026-10-05T17:38:58.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/51/9c583be502720e927560a8414f4dfb7c7de41945e1a30ccf7003b93c6f85/aiohttp-3.14.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6c044e52df1a466af82cad3518c92ea80cc4ddcfea86966ff083bb34bab29bf4", size = 1937772, upload-time = "2026-10-05T17:39:00.376Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3d/b56512df7ea9cbb5f59099a6b375088dd578356a5a3fb27198ab82e19832/aiohttp-3.14.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d613d7d51bf06fe9a5e3aab86ce06d835bb0ceb1a31ea9e8766df4a403ae62a6", size = 2066220, upload-time = "2026-10-05T17:39:02.54Z" }, + { url = "https://files.pythonhosted.org/packages/11/6c/a7a42701d9ea319140a3d44a24ec72a00b0b46d1b899b26d8a705e65bdd8/aiohttp-3.14.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5e93ab6e6330dce40f5b43996f1fc5f2543296d263a5e4b59ff09e73870b7d8", size = 1922895, upload-time = "2026-10-05T17:39:04.638Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/f8337bd824ca3fe3968d34dc5037ace1c4872551c4621f8266239311d301/aiohttp-3.14.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20134dc8e68c67e7478124d516449428684c0545b2d3faa575691c8bcbd56678", size = 1712589, upload-time = "2026-10-05T17:39:06.671Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a6/d20e2c940b592d7c4a21690889dbff64fe0ca3facd9190696368694e1488/aiohttp-3.14.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f0daef5ef012369dddc391c61f604111424aa9a96313a69fb899018b09d1b01e", size = 1862251, upload-time = "2026-10-05T17:39:08.523Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e4/d50edd5ae5d024b380a8e892e333840bb22967a9eb9c8492108a403f678a/aiohttp-3.14.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:19290a108de0718b73bc69b86e788975c6a59855c671ebe2a98a07a6f968328f", size = 1895155, upload-time = "2026-10-05T17:39:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d3/f36f2931f807ccf8437ec15e8f8bfaa4748d4792639401cc767a866abaa0/aiohttp-3.14.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1494877625baeffab669f4a25a09ec4eea721b49e35363081a2a8e4231fe141f", size = 1906623, upload-time = "2026-10-05T17:39:12.411Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0a/94821b711493142bf5e11c27b0d7502776778f7792c77ecc6647357d20c1/aiohttp-3.14.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a779ce4bf099ce4ad2cdfed62f1ca536d435f53237c54ff40172fddada24d1b5", size = 1698932, upload-time = "2026-10-05T17:39:14.446Z" }, + { url = "https://files.pythonhosted.org/packages/d9/76/3d5a0d28362f0053c1c457024749911932481e8fd105fddf8305b1f93c81/aiohttp-3.14.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b4cec9fca876e2d4f4c6cb32b177390132d04618867689f43fefabf3f5c3980", size = 1934528, upload-time = "2026-10-05T17:39:16.354Z" }, + { url = "https://files.pythonhosted.org/packages/a7/51/9784a21dc05d2e0f6acaf864f16db53dab0fbd7b0753cef168601eacb012/aiohttp-3.14.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:217707313e23aedde9bea67cf8365c7b138b549ae179121ef3f476cf67e3173a", size = 1901984, upload-time = "2026-10-05T17:39:18.299Z" }, + { url = "https://files.pythonhosted.org/packages/44/4f/f98c88703e6d620243aa08473a946ca4e3756b6288690e38f0d9f9c1d007/aiohttp-3.14.4-cp312-cp312-win32.whl", hash = "sha256:18a9fb9a3f6e6e63f4d27f9adeef313a2d3c69a84d31170a844623824e722534", size = 480278, upload-time = "2026-10-05T17:39:20.206Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bc/8555013f84b53e4fdde423131b641af50e822c6d7d9118b24c52b31f6900/aiohttp-3.14.4-cp312-cp312-win_amd64.whl", hash = "sha256:69dcb02d33dfe415d5ee342cc63b7d320efcaf4b761701c7bd7bfb91c715481a", size = 506317, upload-time = "2026-10-05T17:39:22.706Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/ca5bad7bfdaa7ddca053a044a1c7c0d4f3ad9d1f12f96dcab10bfb9c50e7/aiohttp-3.14.4-cp312-cp312-win_arm64.whl", hash = "sha256:d36b0263e7c2fbf1750b9f35d4e7e48b0442d8c45e4a89e3e74168821fb014bb", size = 487683, upload-time = "2026-10-05T17:39:24.482Z" }, + { url = "https://files.pythonhosted.org/packages/23/ec/7fee140da700ec3f5aa5e0fd2319f67be5e86de197e2384d27e5e6209e0e/aiohttp-3.14.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:264b5a7568306590c44ae4a0237ad1715e35d447ea44ebf324e4c8fd9f78f67a", size = 539114, upload-time = "2026-10-05T17:39:26.322Z" }, + { url = "https://files.pythonhosted.org/packages/4e/04/930c01ce9e186e787046433d7dc5b45b188885f8406c14b58435b4431974/aiohttp-3.14.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:17e5d7c7775d8dd8e894c7ef6ebfa26509de36868a9a6c7c4fcfaf6a1bf43d60", size = 553514, upload-time = "2026-10-05T17:39:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d7/983e3cea81c07e95f35d34da4d1e182b5c05489c49b7560b198a83f0da79/aiohttp-3.14.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e91dc8fe9cbd052d16f7f1267e285cf8bbac4d2194bd5741f05ef201c77ed09e", size = 521381, upload-time = "2026-10-05T17:39:30.419Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/80baed11659093883f51245755105557d3253e45bd0b693d70df0059d68b/aiohttp-3.14.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8df6157d31703972aec9e3b93963f039ff06318301e30b007c3e941ff8a7ac42", size = 528347, upload-time = "2026-10-05T17:39:32.344Z" }, + { url = "https://files.pythonhosted.org/packages/c8/18/bfda255c311096c08dab26dc65f3d219f090df37dc5f5f6345f8c3aefc1f/aiohttp-3.14.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:220912b351549dc736c59d104dc443fa615753102bb778a4e98f86e4d94bff6a", size = 539914, upload-time = "2026-10-05T17:39:34.265Z" }, + { url = "https://files.pythonhosted.org/packages/51/fc/3b6ca6117c0c42481d719411fff7b5d3970ebd906543d3dc1ba40fe2462f/aiohttp-3.14.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2efa42b39bb3f524d5eca3760639b584afdd1143a6b047fda29910ffb75e4993", size = 813997, upload-time = "2026-10-05T17:39:36.061Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f6/86e3e02a9712aadc1ae0a1076bb2d1adf3d9addfe5542a28190c8f9b6d9b/aiohttp-3.14.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e7356059c8262d2c1fd95242e2b7b70fbc6eee3135bf470049cbec3e968b5b0", size = 548058, upload-time = "2026-10-05T17:39:38.109Z" }, + { url = "https://files.pythonhosted.org/packages/85/7e/6b9c2e271419d40ca76589613b3c7ab185fd3050a407bd2565b6aa46f695/aiohttp-3.14.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fac247d0cc956d732df1211ac2d8a9999ec8dc1a92a90401de9d52e4ead546f7", size = 545933, upload-time = "2026-10-05T17:39:40.193Z" }, + { url = "https://files.pythonhosted.org/packages/14/fc/33d6e24a5f8ac32423230088c415e2a1c6efee23d902b62f20506631e40b/aiohttp-3.14.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2eb0f8b03b4f2bd154ed8cf7c1a119a1ad04808c258bf58f90138c83b8c6d5", size = 1888265, upload-time = "2026-10-05T17:39:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e8/2eca1d468d83a514621d1280f9992d08649191db2028c9236b4edfa7c723/aiohttp-3.14.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbe827d27e0e369fd7bb88ceeaa5b46ea006e73e015b9dd77e3723a53162293", size = 1872300, upload-time = "2026-10-05T17:39:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/5872983de71b0214acdfb86822f5cca295b39be4832ec589fa224aee0f6c/aiohttp-3.14.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70387146d03af9f047c629b71aa27d9594b3b31ec4c9c91e1a73d0d8bf38e0e", size = 1926213, upload-time = "2026-10-05T17:39:46.199Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/ebfefa11a546c33c717af63c67868408ecf0b33b23a9d75669af95485407/aiohttp-3.14.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d0934926fc65744e2fdd44ce68b2d79d5ee608a1e23f0596b35dd696519bb7c", size = 2056609, upload-time = "2026-10-05T17:39:48.221Z" }, + { url = "https://files.pythonhosted.org/packages/69/c0/70667e82ec041f4122a17afce711bbcf94a71bec5888b69a49aed8817be2/aiohttp-3.14.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff4b366510c733974adb7a4102c094e4a6308e6f369315a4cf14d2d0a619cf82", size = 1914740, upload-time = "2026-10-05T17:39:50.304Z" }, + { url = "https://files.pythonhosted.org/packages/bf/86/559e9dfea676206b20737d7ccb5b3716bff5e58d2a8d53682e5301724cd1/aiohttp-3.14.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:85fac7c99e0ac3dcbd6ae1c33774e0603bfc732ddb967a6fce4a731103c1888e", size = 1703390, upload-time = "2026-10-05T17:39:52.385Z" }, + { url = "https://files.pythonhosted.org/packages/fc/91/c98897d4cf23e120b2f3980042835cb34c86bbe3c653f3eaacf4bf986349/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef1e9c3b4a2300023a5dc865889ce532cfaf6798ba4ef78fbbefe738a9d23efa", size = 1852642, upload-time = "2026-10-05T17:39:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/14/13/0258f352cb2c236f358a5b57d138e15fa2566e17accc0758d968dbf2ceb6/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:97e65916588be0f952b3307aaa8460c0771fa98dbea2dd27dabc9f5d84d53c3b", size = 1890616, upload-time = "2026-10-05T17:39:56.466Z" }, + { url = "https://files.pythonhosted.org/packages/64/3c/f43e294ce1a06d1234a97bd862c1aa31acfa1a0eea3786c22fd0dc1ea731/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:840775cb39a9424f9edf142133a1192263ac5bd79117ad2a16602cef6a0c1e9d", size = 1896638, upload-time = "2026-10-05T17:39:58.585Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f7/ce1e3a85b73d05037d729d3d32da7d6959b9e433435e9abd46c2fb38410f/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:54f9256430ca040d58affbd551513de13a5cff7d065204fc52b9a1b0af09a049", size = 1693003, upload-time = "2026-10-05T17:40:00.698Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/45eb900d6a9ee046728f5589f3d07f5d78f049b5fb84f48881fa4174d5df/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6112d5931bf12c776188cab81b82bbb17d86e2e1381f2ade6edc43e6b240f0cb", size = 1927730, upload-time = "2026-10-05T17:40:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/b2/43/a83ff79dc8375953a1f00b808a21c82590f4e68304fecccdcc42a25fb7d8/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:facc6df39047c4732cbd885bb62e40df20885fcd709fc399e11f81b45ce0fff9", size = 1891177, upload-time = "2026-10-05T17:40:04.989Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ba/fbdc41608d4e2fb6084d960a41b392fa82beb5b126c2466de549b5c67fc8/aiohttp-3.14.4-cp313-cp313-win32.whl", hash = "sha256:7968634fa3a967a2bc0b9aeba1d5a1955501dcd7d0de44b96f7ebfb662797735", size = 479943, upload-time = "2026-10-05T17:40:07.402Z" }, + { url = "https://files.pythonhosted.org/packages/35/51/d75629750e705832fd5cd52516a82943ed58b6e09c30e14830f5d87452fa/aiohttp-3.14.4-cp313-cp313-win_amd64.whl", hash = "sha256:c0a894b0265d139cd4c2c8bd4643822cc289a4a5331e21e810881bd000ffb2dc", size = 506209, upload-time = "2026-10-05T17:40:09.49Z" }, + { url = "https://files.pythonhosted.org/packages/02/64/66452d2ffcc13e5e5ee4ba4a968656fa4a98c29ed5ff0205d4fde02b2943/aiohttp-3.14.4-cp313-cp313-win_arm64.whl", hash = "sha256:da16c3037178e72589d91de59536d271dffc92c2be2b47c38a45538cba54fd29", size = 487163, upload-time = "2026-10-05T17:40:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fd/b653b1f969e2e949fe4726af7a8043e8ef117f202bed7757ac8564e077a1/aiohttp-3.14.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:749bfc43bb1df71bbe6ed215044d5412d786698fc5771d2037436ca49622f3c9", size = 540738, upload-time = "2026-10-05T17:40:14.109Z" }, + { url = "https://files.pythonhosted.org/packages/90/b5/eea49a0bb697deffac5165d707ec53a24748d6720b3af77a544107b49573/aiohttp-3.14.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:c113e4b489d711f606ca278cd8dc89179746244ea841aa0638b1b134600afad0", size = 554494, upload-time = "2026-10-05T17:40:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/11/ea/cf8f4efbbfa8572d9965cd43b3c6c84c581aa006772e5ce3ab4020027e9b/aiohttp-3.14.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:50969522bc7022026a2c7968e67da5c789f6b716b988edac55dc630390efd5df", size = 522653, upload-time = "2026-10-05T17:40:18.879Z" }, + { url = "https://files.pythonhosted.org/packages/a3/24/9f6721f0b52323789cb1041ffc3afa729462d9925607e733a065b6fc5e17/aiohttp-3.14.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:cbf92cd7a1c800cee244558dd5630abb45c1c1093ef7b0d546a99f526ff7a9eb", size = 529697, upload-time = "2026-10-05T17:40:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a4/399debc3c2ca0ab88a08e3082b4c4f62d0f66978af60243c63f2345ef337/aiohttp-3.14.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fc76be08ff407fba717a769aff04ca1b4bf9ecfdcb5caadef84a563fdbbd146d", size = 540526, upload-time = "2026-10-05T17:40:23.548Z" }, + { url = "https://files.pythonhosted.org/packages/a2/97/90379813b18b795bccdeb3a5a85e9b93ac2c69f9c2a5d0faba59da3ad6d1/aiohttp-3.14.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9b416f995b68a921069e6ec61fa07f2ac3ff033d92e97a69f673af89f5bb954d", size = 818866, upload-time = "2026-10-05T17:40:25.842Z" }, + { url = "https://files.pythonhosted.org/packages/78/70/824fd0dac819652f1df895b3f621a75bf0a3050c30e38d8d2316e34ab821/aiohttp-3.14.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2c9585101cfca10e74a07cc0ef2bdc6d90616e6750926b20456f429d0608f740", size = 549666, upload-time = "2026-10-05T17:40:28.523Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/eafb2dd200dd2b770ed25a2dc5731c4ba5d58ab3fa8e2e82494bb946808e/aiohttp-3.14.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3c4dcf611b095e7983421f49d01ffbed7ed574891533d606ea115fc23e7c4e90", size = 548961, upload-time = "2026-10-05T17:40:30.613Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/6c733a1c71300293471ac7ef1dffd12b5b4dc9a43c53461224b2b8a7f667/aiohttp-3.14.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8f61a9b5fa56c58e3441f9dc64137f786232d40fb89127190d0e0f84e1e5865", size = 1883165, upload-time = "2026-10-05T17:40:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/34/885ddbd351679a31141b25e0d1a5e4f67738806e564c858b399709b61239/aiohttp-3.14.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d43e538dfc85b705fd832620d050970ef5613b3869a7169b44a554b9dab9eee9", size = 1844119, upload-time = "2026-10-05T17:40:35.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ef/0a64dac53c11d569851efb3e7eddb43551df72b2c172ebb34cccc93bcd3f/aiohttp-3.14.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4135c4bb4b7682a7633fadc7170ea969d843f83aa7224e70111a20675105c985", size = 1930222, upload-time = "2026-10-05T17:40:37.527Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/774792ed8f973e0d234fcb06b0f0bc9f29e9a0a0dd847f6418c2904be7d9/aiohttp-3.14.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9055324f156a94fdf6dc9a825ae60f9a2947b88f591ea81ee80826633de62ae7", size = 2038842, upload-time = "2026-10-05T17:40:39.815Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/e00f35581c97656d9e864edaa03292382c620dd1d9c836a4f73a2279acc4/aiohttp-3.14.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb22c8e4b60385a9b414c70c6e8b2d0f4b23ff7fc4bb572ceb58297b228169f1", size = 1887845, upload-time = "2026-10-05T17:40:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2e/07e3eff5b4c1b082a2eaf0ffb508b409b21ea1ddc9fdc57f3eabb51469a1/aiohttp-3.14.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b6945559a260112742ed58cde2165488d2fc51aeea34afbaa99d9371b43e1b5", size = 1702533, upload-time = "2026-10-05T17:40:44.569Z" }, + { url = "https://files.pythonhosted.org/packages/75/44/816844b7f876e6f8fee4ecb088269fcd92241da712e4814a4d624eda13a9/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f4078a90b174f15efb2c8725e8720c40dbd3b2cf6350dbc085c964a938d9de7", size = 1849058, upload-time = "2026-10-05T17:40:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bd/163242e12464a1f3e58a77e28beca949652ddce84abdd5500a91c70a1e0d/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f433c5654e32f72de3ef074d715eb5c7604eb6d28a5c2f3c9e5056f20fe6b306", size = 1854600, upload-time = "2026-10-05T17:40:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/f2/59/5c1ae708a89524eddfd9c727b87cae26b5c50c3c76683b910ac06151ead0/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:871035fe79c636b6bc126ed78df37d72b830dfc751612cb4b1040360edf2719f", size = 1896565, upload-time = "2026-10-05T17:40:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/37/45/95300a7de69241fe47d3f838f3fb582c712782e172d447ff650a013ea673/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2617132524f66338f6c1d218cd16081576aefc40df4f2bcba338a99fbf67fec1", size = 1693092, upload-time = "2026-10-05T17:40:54.862Z" }, + { url = "https://files.pythonhosted.org/packages/ff/33/01a8e61d48d0094121092c74125df14c0f8f10ee3149bfe68b6f869409a9/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d8bc326133272deee8ef337581550450e4bb735c0520be893b950a1f5baaf41a", size = 1917073, upload-time = "2026-10-05T17:40:57.656Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d6/1a4f24624cda6686d5b927e26d5d1ae3ebb692faf88e93017fab4b391fd0/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ec57223e261b7fa9979c12dbce009ca53439be76cac3af27f860d3e4b1d6f7a", size = 1872656, upload-time = "2026-10-05T17:41:00.123Z" }, + { url = "https://files.pythonhosted.org/packages/be/be/ed286fcfa2d22061ae021b568c790123505cd482df35e23023339ecaed96/aiohttp-3.14.4-cp314-cp314-win32.whl", hash = "sha256:7242043e71fc449a19a47d92f4e9fd1e0ae8d3f88488982381f422bf6d651218", size = 485461, upload-time = "2026-10-05T17:41:02.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/74/e040e6f65f4da305777f1adb3023d47401693489e4a337c983b52d2a0a6b/aiohttp-3.14.4-cp314-cp314-win_amd64.whl", hash = "sha256:019587c7b3a44917dd61bcb312fd5c034f341dc9851b83e7b41b65e08b68ca0f", size = 512147, upload-time = "2026-10-05T17:41:04.98Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5e/bddb33ae15fdd5a68a10a986587aa9916c418a712fdd110d3b3ae9b51407/aiohttp-3.14.4-cp314-cp314-win_arm64.whl", hash = "sha256:60dc71d38db0988f37e78f6ad4a010a2f6355cfac3fff5f130cf5ef2231f3f0d", size = 494685, upload-time = "2026-10-05T17:41:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/22/2e/7b884014b40573f919fc3a528e5028bc37b249e31a93928ba61bdd25c960/aiohttp-3.14.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:674f53ab9d2794e2dd767619a3384bf1006f8d075e43c19d449a645f8ff641c2", size = 855696, upload-time = "2026-10-05T17:41:09.514Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e6/b6e2b4f580ab6ae39241bb9182aca60e6e1915cdad0148faaa04db3f366b/aiohttp-3.14.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6d243221f73e5ac546c2ed9255100423e4bc092b08b944f23498e8cf3096c26c", size = 567931, upload-time = "2026-10-05T17:41:11.685Z" }, + { url = "https://files.pythonhosted.org/packages/63/99/ae8e3a1a18adcbf636839d18b3cccc51007836ccdae5209381c5cf82d1ae/aiohttp-3.14.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0daa016eb74e7888ed5f9ceec6eea3158999c1c4a115ef5cbe88baa8d924f94c", size = 567421, upload-time = "2026-10-05T17:41:13.836Z" }, + { url = "https://files.pythonhosted.org/packages/63/38/c5d4bb58be72b967a4090d2deeb38571f8c992852936c51dbbbeae49db1c/aiohttp-3.14.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70c031a622aa20add05ebd234b90adaf6bf51c1c88cc305ec760721af83308c3", size = 2054173, upload-time = "2026-10-05T17:41:16.098Z" }, + { url = "https://files.pythonhosted.org/packages/eb/87/3c8a9c7670f56d478a75c5bb84b336fb61a67b908e36c30a4a96ed8c8e86/aiohttp-3.14.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7df363fa7c38945a896e12c69af4a1a56cbd4152791d9e41fd0bac396b72de38", size = 1918296, upload-time = "2026-10-05T17:41:18.478Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/bd00e499c0a1505b44f5e23b9739034ea9be792ad42537f2894211ba1c07/aiohttp-3.14.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02a508ba5ad91c4d730523584c0adc65882274bba5e37c33bc2f475e37577256", size = 2027513, upload-time = "2026-10-05T17:41:20.874Z" }, + { url = "https://files.pythonhosted.org/packages/23/1d/a3666a7c224f86a881f18d673a8ffdc92fa4c35274fb4361822f8f0dc566/aiohttp-3.14.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0cb3edece6fe6a944eaf97d803b88f606da6bc1600b1894c2969e78ffdc1fc29", size = 2152686, upload-time = "2026-10-05T17:41:23.181Z" }, + { url = "https://files.pythonhosted.org/packages/46/7e/f4204b8c4959c46f6367c182afbbacef08a1b53874281912c52fbd80151c/aiohttp-3.14.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21ce1223b461bcc5fb389a7204001578f1c0b85c2da60a84d915ebb021294245", size = 2006431, upload-time = "2026-10-05T17:41:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/33/43/091e69d42967cbea833ceb355c5457f8f70748fe08b88c9d91491b3fafb8/aiohttp-3.14.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35d5432f0dc1c8b71307e9112461b52f336da225e344283eec4779222c0a1dff", size = 1789101, upload-time = "2026-10-05T17:41:28.095Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/b57cbe533c01098c5d30471f057e7c3c19f281122f756acc9c716b636e2c/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6ec364493057c118d08292f7cd60865965c209167d8657b45643a26aef77495", size = 1974472, upload-time = "2026-10-05T17:41:30.499Z" }, + { url = "https://files.pythonhosted.org/packages/9f/44/79acce03611450e4ed8594f96f1cea40d70bc84fb4e918f94d7d9454a4af/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:70492983b7aed9e61bda82fe1ce80bcba9199610886443133783a288638923c0", size = 1956340, upload-time = "2026-10-05T17:41:33.009Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/36e6cbd6720e3377a02f8709aec6fe80de729ef41e78ba01f13d50363c73/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a77776673331577147e9d25cd7833a72e8891b47556336cff06ba1569e488b99", size = 1965771, upload-time = "2026-10-05T17:41:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/69/0f/879735ac70ce5e6852916eed51c706a517b205150a8d384f9fa60933a3fc/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:73799000dd6fd5247094fd8e6d3e1eb871e41aab5cc35dff9d9f654cd230bc24", size = 1780297, upload-time = "2026-10-05T17:41:38.284Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/7c030ca5b6baf3b02eccd11afc3d58f0b35e407031d5cc78cae9c24921c0/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:27b370fb532e707e8c96d04fa0c96301fb7dba2d50aaca57f6c456cdfffec5bd", size = 2001374, upload-time = "2026-10-05T17:41:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/ddacbb8141d727034c51dd192d17749521d5b30b03f84aa034325396089e/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:884e0d1c707b6217fee3080b9b4029eee7551af788ee761010df19be158414b7", size = 1968531, upload-time = "2026-10-05T17:41:43.62Z" }, + { url = "https://files.pythonhosted.org/packages/3a/08/2214f823701eebc3e57db141faeae76df7bfd415f254cc7c1692258f228d/aiohttp-3.14.4-cp314-cp314t-win32.whl", hash = "sha256:f2e3d34edb151d3e27f93a8338e2da33e2c7261cd4c879ce5f5fe280cc190ea5", size = 504126, upload-time = "2026-10-05T17:41:46.321Z" }, + { url = "https://files.pythonhosted.org/packages/91/a1/90e9b6fd23310b4e82d0c0a37b5ecc2944053350473c57533c1da9647235/aiohttp-3.14.4-cp314-cp314t-win_amd64.whl", hash = "sha256:88cf889e51092537fe44adfbff96c2a22d6479d6f92b97b3d413fbfd0a4decf4", size = 531494, upload-time = "2026-10-05T17:41:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/5b501472c92b9b1dc53742591d6210dccb262962f4721299300d3a9f664b/aiohttp-3.14.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ca450c6d42fda7c0bfaf0e200f32f77c5f2c960a33be1e14d197c84588b7f5f6", size = 510544, upload-time = "2026-10-05T17:41:51.358Z" }, + { url = "https://files.pythonhosted.org/packages/0f/77/ef165a197850573ea07c8d77bf5f4854a1db12fdb20e1cf0b218abf025fc/aiohttp-3.14.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:3b10799df72f66c30935bc661cb89e5ac2918debfc575e350e226fbaa846cbb7", size = 540737, upload-time = "2026-10-05T17:41:53.761Z" }, + { url = "https://files.pythonhosted.org/packages/e6/aa/1663f64d1316b3be9d3f6fafa02b8d888070171a6544cba515e82f3af595/aiohttp-3.14.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:7ffd7f9dd39caccc6b2f68ec0339435ecad2b20f6ea2e2b3de91bbe464ac7137", size = 554600, upload-time = "2026-10-05T17:41:56.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/f7/d4d535855123c1a774a798643e75fdfbac62c047b0c0729d6505d09df444/aiohttp-3.14.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:a0a7a227353f7693f73878ad14cd2623cafadd656ababcd20e2adfb7225172ea", size = 523184, upload-time = "2026-10-05T17:41:58.624Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/18a8d1cd2958a0b6164b047c18ccb192f2123bb70f1c0a145f6f01f858ad/aiohttp-3.14.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1c43b6af8e4708e8b3812a9ee4f79895207e12d555b7f6d70deb838e0011e12d", size = 530342, upload-time = "2026-10-05T17:42:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/d1dd2b1fcf8a3192a431375b287a8762ebfd22195ab5b201d3fac30e3a7a/aiohttp-3.14.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:97416b2b997e5e9b817e322b2f0a89b76c54cde22471f51ac17ac15e10901ba0", size = 540629, upload-time = "2026-10-05T17:42:03.324Z" }, + { url = "https://files.pythonhosted.org/packages/37/51/d1868f3459f618f6b68a6ef549d8b3f76aa616fa083298911fd3ae65d6b9/aiohttp-3.14.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:ee2a5aa31e507c17841d592364f2148393b17f83bc2766e9211e1eb31ea72a70", size = 819347, upload-time = "2026-10-05T17:42:05.713Z" }, + { url = "https://files.pythonhosted.org/packages/84/e6/53179b721b67e0bd20974d02eca97b5691559e8ce666ba23a65aecda6b7f/aiohttp-3.14.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:5d5d9843c34866e986dd24eef713452609e304550c471320ce7b54b3cfb1391e", size = 549825, upload-time = "2026-10-05T17:42:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/00/0f/d67ddf7a43d4899176954511b6c048328abec1a5df4901f811a3b408a4fc/aiohttp-3.14.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:efcbf93e31b52c21665b6ebc489eb6fdb0dfd86b592edc677b5cffdfa7b11677", size = 549315, upload-time = "2026-10-05T17:42:10.766Z" }, + { url = "https://files.pythonhosted.org/packages/9d/db/42f372c7f8e259505f239cb49db75f23548deaec56d22c68b4866f1c3fe2/aiohttp-3.14.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f1a79977794592dcf6485ed964ec2448db527a1bc0c72ec89663658dad3a739", size = 1892181, upload-time = "2026-10-05T17:42:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/23/0d/8b1900216302a94f69b41bf0786bb992a6e67d703f4698e335c2442db615/aiohttp-3.14.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:515e7ac6d27509dc3ba2c35fb88970b00b9e7b07ee7cc947c8733dcd54ef5def", size = 1883513, upload-time = "2026-10-05T17:42:15.704Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/15c014313e567fff931ec4a5f81683cc40251257791fbd607ec37c0dad80/aiohttp-3.14.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:185154ffa3b54765b5de96f464f41cfa476f8815a03854ae5e0fe4d48fb06e9c", size = 1933283, upload-time = "2026-10-05T17:42:18.241Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b3/bf7dc39a450218efb1d5c692bac7dc85ef56da282e889bee594f3267211e/aiohttp-3.14.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6aece59f77959628a22cd2ffe48b102cda9611d680e090f204385b3998a1190f", size = 2041054, upload-time = "2026-10-05T17:42:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b6/03/ffba18d7aaedb5eafac3f41f0c26621ebb0a740bb0115ea2fae1911d902b/aiohttp-3.14.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7205aa6589f1ad1306c6994ff2e1ab5efabacf938f9ffd7682fb225c197b06a", size = 1895494, upload-time = "2026-10-05T17:42:23.518Z" }, + { url = "https://files.pythonhosted.org/packages/45/f4/68123365b86826eee82bdceb7fb4c429204cb3a104d6b7ae44bc6a543fcb/aiohttp-3.14.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f38c7b3ce60d55ac0f2a9fd2edc194789d043fefea15cfb7dcb40b7d5f561146", size = 1714098, upload-time = "2026-10-05T17:42:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/b13c0b214afc571782e8667f3b9e0bbed6843397be90fa35acf2f3ecfbb0/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:b0a1274ee4ee6203c15a341cce87f9f3df8151d5496047b3608bb62bd969cffc", size = 1857111, upload-time = "2026-10-05T17:42:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7f/4f5d3cee3c23b57119aebba5174962fed0bc493f37f38edab2f5730411b6/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:69ce2407951516f413adf3132b88451eababd710ce631f83c81a9603a3640253", size = 1892123, upload-time = "2026-10-05T17:42:31.786Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/5a6a1f014c4e17d58d4e935d0dd64e612f4eaa2f33cca2475b48ca5e3fb5/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:b84d3d057c60eb8e0c2b457a1203e868d82709d3f0c272a4975dfd88c2414939", size = 1898809, upload-time = "2026-10-05T17:42:34.34Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/d1cca5311a70fcc1c7c2082e02044578a69bcb10686db33a322df9fb849e/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4ecc84f69c9e217984f6877e832e4786a74567432c0bf2b7512919fbdfe18ade", size = 1704814, upload-time = "2026-10-05T17:42:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/0a/cb/cd58eae366148a5b9b1baa233542151154f2b72cd32daa39fd9db97d0c0d/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:878f2d7950238b2b0a8cbb88054444a125f0cfbc77f54c811aca0483132a9be3", size = 1918390, upload-time = "2026-10-05T17:42:39.674Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d8/f5e6c4939432734033048ba631503edf4a1e693ee5e261773e169803bbe4/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:6ff9d3a0e6935f1cb0a6685f9f4b3f0d3e406456d7a434a03cf309b8cd5d7a2e", size = 1878321, upload-time = "2026-10-05T17:42:42.528Z" }, + { url = "https://files.pythonhosted.org/packages/2b/71/462957d6b5d4eb208f78e67745c41480bad2e738aa7caf1d9b07536920c1/aiohttp-3.14.4-cp315-cp315-win32.whl", hash = "sha256:e07dfd7cd360f20be26dc2487ad4c7f21b0b39fdd593e9ebf37d2424ad52defb", size = 485536, upload-time = "2026-10-05T17:42:45.223Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6e/48f59266a8b8c6420894ff59b8e74175252da67df85967b96997e96b7092/aiohttp-3.14.4-cp315-cp315-win_amd64.whl", hash = "sha256:921fc4f1ad549091bfc39cb5e93925b04f307f83517756a3111843851130efce", size = 512087, upload-time = "2026-10-05T17:42:47.717Z" }, + { url = "https://files.pythonhosted.org/packages/25/f5/46263b23cee4f312e28aaabbdec2e074c807700a12c9dfa18beff461f035/aiohttp-3.14.4-cp315-cp315-win_arm64.whl", hash = "sha256:5be8af106f96fd625f6c264fa9faaaa9e0104089684d9015d2fa5b6eba6407c7", size = 494709, upload-time = "2026-10-05T17:42:50.615Z" }, + { url = "https://files.pythonhosted.org/packages/77/8d/e3b1d406c95a8fe436ae2e5f01ee9591196c6c22c44c645b24f20d73eaea/aiohttp-3.14.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e69727ec04a4608200ff32ec29478299df7657e2eb78c416ae338d0ca6662596", size = 856014, upload-time = "2026-10-05T17:42:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/5d/fc/e9e78d1097830040bc0a4b8af073379f964064bfa7f0a852d774f96f3168/aiohttp-3.14.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f8b658dd3ef2ebd7708b311ed96ff7cb5edaca001a30018a091504ef492bcb1c", size = 567724, upload-time = "2026-10-05T17:42:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/c813a705a1da1c111c30723b52e1efb67763e9f0b3b54a418cb419616e7a/aiohttp-3.14.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3749e018123df1205161a73ba96d336e17e9cf6b947295e53a43571de420e7b1", size = 567888, upload-time = "2026-10-05T17:42:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1f/7676f46aa24554a45686853c61552d02d67ade2b4c51adfe8b2ec4fdecd2/aiohttp-3.14.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f28a3c4fca436b2e594090e7cd22558f5e93a30ca18516c436de982da6fc25a", size = 2054356, upload-time = "2026-10-05T17:43:01.781Z" }, + { url = "https://files.pythonhosted.org/packages/44/79/b9ef7d7442061e0a3125990d6021230b30361a972f7b8ebfb011042877ce/aiohttp-3.14.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e099aae21246992a1d84303ff262262d054c474e46cace33b0d5a69810c71877", size = 1937678, upload-time = "2026-10-05T17:43:04.492Z" }, + { url = "https://files.pythonhosted.org/packages/5c/98/5927a31540c8e6d0542effead5bd5c56e749a9d99d84e902fb80ae906ef8/aiohttp-3.14.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fafb521891e646d9cf5da89f01c75e7159d08be7254a024149630a698be28251", size = 2026705, upload-time = "2026-10-05T17:43:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/62/bd/1be001ed97dcfa57a292188c38979fb78be795b960b50f02660587bffa06/aiohttp-3.14.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1d385db2cd154bce0b3dd451cf2a52bec91ebb7be2c3a5d06b14b4391631d807", size = 2147075, upload-time = "2026-10-05T17:43:10.141Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f7/d59628ccedd7c86db4c1f904467d1c80fed6cc10a1831ed5d75ff12e5a71/aiohttp-3.14.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae8a244889d8a53549851bf595b9f382f64acca57e8762da1ecb26ba786828f7", size = 2005016, upload-time = "2026-10-05T17:43:12.848Z" }, + { url = "https://files.pythonhosted.org/packages/3c/dc/472db3ab1daafa12a61d7f83314f85f502b99c143c78d66554a5e24361fd/aiohttp-3.14.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:681dde68ff8d8d5e7d5abcad4feb45bfb457a218310c919825e21fc36cac3d0b", size = 1796363, upload-time = "2026-10-05T17:43:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/08/61/6f9834b131b9a42e30d68c469c196033d3cae2d8fe340d4668f362c4fd1f/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b91a6441fcaed88ca7bca67726b189332df3ceb6ac0dfc98dcb9654bb723288c", size = 1974240, upload-time = "2026-10-05T17:43:18.884Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/b8eb2a276685ef1da7f7bc15348404b8b9ee12b8f32f2cd8895385cd1589/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:f15dbbac49bc15eaff95c0b8166525c4050996af578c7be141e145f40ee2cf24", size = 1973387, upload-time = "2026-10-05T17:43:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/5b/32/ad559b2d6817e85f7cb294d8641e975e64b526f4da98ffe941c9af67a047/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:7abae043e87692d1c2adda1fab501bb0612d28c4a23eb082005c1e684f792dac", size = 1968766, upload-time = "2026-10-05T17:43:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/60/67/4a16e398140bdabb363428b9f439af3f77706a6df8bea6e626bbde53c2ab/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:21f4624fb1051fc1aa10d57a636a80e829f7d0417462d6f1ce380b08fe1a904c", size = 1788193, upload-time = "2026-10-05T17:43:27.467Z" }, + { url = "https://files.pythonhosted.org/packages/a9/38/17b326da7f52e530a675d789ade3e727a1b3db9959722f6049b9c392321a/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:2e8dc3a0fdaf9d12b954d3d06cc4c355d91b17fb6c671715b593624ca4c379f8", size = 1999645, upload-time = "2026-10-05T17:43:30.32Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/91b87961250010e05eb3adabaf0d98e4229ee7543147c265e834922e1b0b/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1410b6f6e3d52ea7c02c950496522ce6e85d5543bf154bc80a6141e505251cfa", size = 1970998, upload-time = "2026-10-05T17:43:33.364Z" }, + { url = "https://files.pythonhosted.org/packages/72/66/9bcf2c2db7b75a3b51c11470ceb67941d028727d674276ff5d5120adc87e/aiohttp-3.14.4-cp315-cp315t-win32.whl", hash = "sha256:a65189a89f3e621f7c27a968b5131bbca29126e2b8517dc72b205747a39d4c96", size = 504036, upload-time = "2026-10-05T17:43:36.212Z" }, + { url = "https://files.pythonhosted.org/packages/e2/20/349db4af12dbc16070d32028d30c2067123b4520f3dcb2d9013524efeb4f/aiohttp-3.14.4-cp315-cp315t-win_amd64.whl", hash = "sha256:2b2c95f5f769eec92b552969db1f79ea0282058ce5131ed0f2fa540b4af3f14e", size = 531284, upload-time = "2026-10-05T17:43:39.362Z" }, + { url = "https://files.pythonhosted.org/packages/26/21/544c711bc5fad611278ebd153bdcab1bffce8b8581f8ec9096b275948a3c/aiohttp-3.14.4-cp315-cp315t-win_arm64.whl", hash = "sha256:54209fff79346ee1ef0d5cbf92fa80a5bb37396406bb4d932e5107f9c29b2d3f", size = 510623, upload-time = "2026-10-05T17:43:42.15Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/5e1f8446545be98e9416af59c396b1ed1f9f3294afb0a40ee3a709889273/aiohttp-3.14.4-py3-none-any.whl", hash = "sha256:5c6758ba62aea282c537179cfc8474a90f2b5f7b089cc5ff66d8920d86a9bfdd", size = 279487, upload-time = "2026-10-05T17:43:44.905Z" }, ] [[package]] @@ -195,16 +239,29 @@ wheels = [ [[package]] name = "anyio" -version = "4.14.2" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] [[package]] @@ -234,6 +291,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + [[package]] name = "backrefs" version = "7.0" @@ -247,23 +313,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, ] +[[package]] +name = "binaryornot" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/4755b85101f37707c71526a301c1203e413c715a0016ecb592de3d2dcfff/binaryornot-0.6.0.tar.gz", hash = "sha256:cc8d57cfa71d74ff8c28a7726734d53a851d02fad9e3a5581fb807f989f702f0", size = 478718, upload-time = "2026-03-08T16:26:28.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/0c/31cfaa6b56fe23488ecb993bc9fc526c0d84d89607decdf2a10776426c2e/binaryornot-0.6.0-py3-none-any.whl", hash = "sha256:900adfd5e1b821255ba7e63139b0396b14c88b9286e74e03b6f51e0200331337", size = 14185, upload-time = "2026-03-08T16:26:27.466Z" }, +] + [[package]] name = "biscuit-editor" -version = "3.5.0" +version = "3.6.0" source = { editable = "." } dependencies = [ { name = "anthropic" }, { name = "chardet" }, { name = "click" }, + { name = "cookiecutter" }, + { name = "dap-python", marker = "python_full_version >= '3.12'" }, + { name = "debugpy" }, { name = "editorconfig" }, { name = "filetype" }, { name = "gitpython" }, { name = "google-genai" }, { name = "hintedtext" }, + { name = "legacy-cgi", marker = "python_full_version >= '3.13'" }, { name = "mistune" }, { name = "pillow" }, { name = "psutil" }, - { name = "ptyprocess", marker = "sys_platform == 'linux'" }, + { name = "ptyprocess", marker = "sys_platform != 'win32'" }, + { name = "pydantic" }, { name = "pygments" }, { name = "pyperclip" }, { name = "python-tkdnd" }, @@ -291,6 +371,7 @@ docs = [ dev = [ { name = "git-changelog" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-dotenv" }, ] @@ -299,18 +380,23 @@ requires-dist = [ { name = "anthropic", specifier = ">=0.117.0,<0.118.0" }, { name = "chardet", specifier = ">=7.4.3,<8.0.0" }, { name = "click", specifier = ">=8.4.2,<9.0.0" }, + { name = "cookiecutter", specifier = ">=2.6.0" }, + { name = "dap-python", marker = "python_full_version >= '3.12'", specifier = ">=1.0.1,<2.0.0" }, + { name = "debugpy", specifier = ">=1.8.0,<2.0.0" }, { name = "editorconfig", specifier = ">=0.17.1,<0.18.0" }, { name = "filetype", specifier = ">=1.2.0,<2.0.0" }, { name = "gitpython", specifier = ">=3.1.52,<4.0.0" }, { name = "google-genai", specifier = ">=2.12.1,<3.0.0" }, { name = "hintedtext", specifier = ">=0.6.0,<0.7.0" }, + { name = "legacy-cgi", marker = "python_full_version >= '3.13'", specifier = ">=2.6.0" }, { name = "mistune", specifier = ">=3.3.3,<4.0.0" }, { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6.1,<2.0.0" }, { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.7.7,<10.0.0" }, { name = "mkdocstrings-python", marker = "extra == 'docs'", specifier = ">=2.0.5,<3.0.0" }, { name = "pillow", specifier = ">=12.3.0,<13.0.0" }, { name = "psutil", specifier = ">=7.2.2,<8.0.0" }, - { name = "ptyprocess", marker = "sys_platform == 'linux'", specifier = ">=0.7.0,<0.8.0" }, + { name = "ptyprocess", marker = "sys_platform != 'win32'", specifier = ">=0.7.0,<0.8.0" }, + { name = "pydantic", specifier = ">=2.10.0,<3.0.0" }, { name = "pygments", specifier = ">=2.20.0,<3.0.0" }, { name = "pyperclip", specifier = ">=1.11.0,<2.0.0" }, { name = "python-tkdnd", specifier = ">=0.2.1,<0.3.0" }, @@ -332,6 +418,7 @@ provides-extras = ["docs"] dev = [ { name = "git-changelog", specifier = ">=2.9.4,<3.0.0" }, { name = "pytest", specifier = ">=9.1.1,<10.0.0" }, + { name = "pytest-asyncio", specifier = ">=1.4.0,<2.0.0" }, { name = "pytest-dotenv", specifier = ">=0.5.2,<0.6.0" }, ] @@ -441,7 +528,7 @@ wheels = [ [[package]] name = "cairosvg" -version = "2.9.0" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cairocffi" }, @@ -450,9 +537,9 @@ dependencies = [ { name = "pillow" }, { name = "tinycss2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/07/e8412a13019b3f737972dea23a2c61ca42becafc16c9338f4ca7a0caa993/cairosvg-2.9.0.tar.gz", hash = "sha256:1debb00cd2da11350d8b6f5ceb739f1b539196d71d5cf5eb7363dbd1bfbc8dc5", size = 40877, upload-time = "2026-03-13T15:42:00.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/80/db62c0a96d2e55282c83524f6b1d02f09c7fd7f612e93bf83e30de1dc75c/cairosvg-2.9.1.tar.gz", hash = "sha256:861bc28ad97ce4f537d50eb3d6ee97a7afcccec9c61ac25c4e7d073fe409aec7", size = 41256, upload-time = "2026-09-07T10:35:09.563Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/e0/5011747466414c12cac8a8df77aa235068669a6a5a5df301a96209db6054/cairosvg-2.9.0-py3-none-any.whl", hash = "sha256:4b82d07d145377dffdfc19d9791bd5fb65539bb4da0adecf0bdbd9cd4ffd7c68", size = 45962, upload-time = "2026-03-14T13:56:33.512Z" }, + { url = "https://files.pythonhosted.org/packages/41/51/8041c2e70649e5b7f2a0aedbbbd0609ac099cfaa0cbde2014279c9c05756/cairosvg-2.9.1-py3-none-any.whl", hash = "sha256:f91c5628e834be024a0ed4544d76261cd84016a4c73bcdf26c386495825c05a1", size = 46165, upload-time = "2026-09-07T10:35:07.952Z" }, ] [[package]] @@ -725,61 +812,93 @@ 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 = "cookiecutter" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, + { name = "binaryornot" }, + { name = "click" }, + { name = "jinja2" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/03/f4c96d8fd4f5e8af0210bf896eb63927f35d3014a8e8f3bf9d2c43ad3332/cookiecutter-2.7.1.tar.gz", hash = "sha256:ca7bb7bc8c6ff441fbf53921b5537668000e38d56e28d763a1b73975c66c6138", size = 142854, upload-time = "2026-03-04T04:06:02.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a9/8c855c14b401dc67d20739345295af5afce5e930a69600ab20f6cfa50b5c/cookiecutter-2.7.1-py3-none-any.whl", hash = "sha256:cee50defc1eaa7ad0071ee9b9893b746c1b3201b66bf4d3686d0f127c8ed6cf9", size = 41317, upload-time = "2026-03-04T04:06:01.221Z" }, +] + [[package]] name = "cryptography" -version = "49.0.0" +version = "50.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, - { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, - { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, - { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, - { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, - { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, - { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, - { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, - { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, - { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, - { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, - { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7a/f08d34ce09d60f89ebd391e2ebc6ba2b995e6dd7552f41820f8085f94e53/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:92e665960f25fcdc73725b9cec7a3824f279ba97a98653afe9ffac2e43668f67", size = 4716035, upload-time = "2026-09-30T15:29:48.681Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/e18fb65592451a2acb76e9f2fbe14e0f47a8318b4c5430f1633851d03daa/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eef4c2f3423810b3070ab391f85436d2f8bbfcb286ac15cbc73190b3563b1f1a", size = 4726917, upload-time = "2026-09-30T15:29:50.608Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/38fdce17e60f6b825e69fc3b7f75e70a6612759980704697e1de4cbfaf6e/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7c6d0330c472d96f6a6afe24d80dfdf15176c33096f0a4397ae4c60f3dd3be48", size = 4715341, upload-time = "2026-09-30T15:29:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/d9121a717e0f893c64bd6ca7702614778d7df2a5c309128a002421788516/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:1ba34f04897fcdaa73f74145c25f3ec146fbd56593853e88adc2e811303c5f42", size = 4726322, upload-time = "2026-09-30T15:29:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/e6d153808bf353e152abd2fd4d8f09670d956ac78379ac46e60d7efbf04c/cryptography-50.0.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3dc4fd8058cea1644971207d530e1a03a184a805ffc8ebdddf0599d78a331b81", size = 3873097, upload-time = "2026-09-30T15:29:56.097Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/1271f287ff7170ddafc2aad36260c4eec20ccd2fea70f38455e9d56d427b/cryptography-50.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452", size = 3805376, upload-time = "2026-09-30T15:29:58.729Z" }, ] [[package]] @@ -795,6 +914,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, ] +[[package]] +name = "dap-python" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "debugpy" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/db/acbf1316e9c579801e7d8da79243838761b534bd26401f1d6fc467922577/dap_python-1.0.1.tar.gz", hash = "sha256:fbe17ae816fd94d627ac4a1522d41b9a8d51f28886e097c016a8b2e435fae6b1", size = 13521, upload-time = "2026-02-02T12:51:50.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/84/2d5ebd54c12c4359f872c99d4a1222dcec2b328f0bc7a1e5390991db1214/dap_python-1.0.1-py3-none-any.whl", hash = "sha256:2888503a8e4b89c6224c2d1a8f8f1303bf749e11d53727f90e5ad4bceb92bece", size = 16094, upload-time = "2026-02-02T12:51:48.093Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/9d/3cb6693342acf96802dba89934a5b8da43c201d764ebd1f2c0514a3d42bd/debugpy-1.8.22.tar.gz", hash = "sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e", size = 1710877, upload-time = "2026-09-15T21:44:02.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e9/ced401ba1088e1d2b3228dbbcae33e15aa8c08a1368433bedebfa8efcb02/debugpy-1.8.22-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735", size = 2128369, upload-time = "2026-09-15T21:44:04.775Z" }, + { url = "https://files.pythonhosted.org/packages/4f/97/5c8e839a7f73e92cd4a733408bb85bb764e829e52a9fb097f99d6e2cbbc0/debugpy-1.8.22-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee", size = 3022081, upload-time = "2026-09-15T21:44:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/11/ed/fbb045d18a8b733a798bb834a03a20cd64f1446206d844b674bb638da6c3/debugpy-1.8.22-cp310-cp310-win32.whl", hash = "sha256:56b877b37ed73f0bf53ba7afc394816ff1eb5d701bae24a24ed9e1ea6f7ce34f", size = 5325298, upload-time = "2026-09-15T21:44:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/82/7f/633160132b80b424c6816391f239682435c6b77cdd58288d04b3443b9f80/debugpy-1.8.22-cp310-cp310-win_amd64.whl", hash = "sha256:1bd0c6df3c68c0a3f71db8baa3780a953abb65537ec4b3bc6b935ad5b3b3d45c", size = 5353598, upload-time = "2026-09-15T21:44:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/26/4a/8c24c588088c622df6dacdc0110e9cc81b44781fe7a408113441e76aa955/debugpy-1.8.22-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:66e4ac3d6e7026e83e7d93d7ee2f51dd4a4e8dff673578d424e60796893e5b2c", size = 2217799, upload-time = "2026-09-15T21:44:11.57Z" }, + { url = "https://files.pythonhosted.org/packages/c7/14/1c9ff33eba51da70a8dfe2277cc13153bb3ffec79282b9acfcdbe1641d30/debugpy-1.8.22-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:192b73e8d53bbd60225220c0943627bf249ac93d0ea3090e23c45f3e0ceb6a35", size = 3074375, upload-time = "2026-09-15T21:44:13.349Z" }, + { url = "https://files.pythonhosted.org/packages/10/e8/1fa16a94af7d8b86c3ca336d7d32b78cb2c475266d0bb889732d2bc50310/debugpy-1.8.22-cp311-cp311-win32.whl", hash = "sha256:745e1800ec2961e5660c1a317c0a20e28c0fef4de36c04f17a21c33f2b37a92a", size = 5258392, upload-time = "2026-09-15T21:44:15.182Z" }, + { url = "https://files.pythonhosted.org/packages/32/1a/c086b883a4561017bfdd83c1bc70bf2b05ab6540ff767dedf207cadab938/debugpy-1.8.22-cp311-cp311-win_amd64.whl", hash = "sha256:1e76339d5510bc17e9181dba9577508afcb21aad5728f1a55ef74d7d97d255f3", size = 5277955, upload-time = "2026-09-15T21:44:16.986Z" }, + { url = "https://files.pythonhosted.org/packages/73/21/5c7f5c66eaeee57c9dadb0bbff22efddc8d99cdbd3d036c806175f1828cc/debugpy-1.8.22-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ad076f4f66cb8acb79e4384d48b1ea60a0b1957aa0b1112d4887ea3a4df60e0", size = 2499375, upload-time = "2026-09-15T21:44:18.478Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ec/7389798843643ea5bc6a08480ba1b855377c70e2ed90189c0a03594e401e/debugpy-1.8.22-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8a697acec45dbc70d17fb5d9f4f61989fc294d273c69de487fd10cb35fdd75eb", size = 4000941, upload-time = "2026-09-15T21:44:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/20/f4/64ce7a2499550929e08654360b270409eeda3e321b7c27027d9c919aa36b/debugpy-1.8.22-cp312-cp312-win32.whl", hash = "sha256:12bc7f368182b517cf26c76a2393fff65354e365fa1552b6241e66edff17997b", size = 5359040, upload-time = "2026-09-15T21:44:22.218Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fe/e7d3b72874cdd327bda295d1995a01e426857ae9cbaaba74fd997bc3afbc/debugpy-1.8.22-cp312-cp312-win_amd64.whl", hash = "sha256:371a4ba4a5975eb958393903f3254cf983da7b1c7c178b3f987ee427f42515e3", size = 5398304, upload-time = "2026-09-15T21:44:24.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/fee6d40f5865aa84e1100c9df1a0f2cf8991776b61f11f433ff277df4415/debugpy-1.8.22-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:c21dd7e1ec22556bb41dc3bf6b86c603e440ec889c4acb27d7206354b2106c54", size = 2493531, upload-time = "2026-09-15T21:44:25.835Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4f/8df91270afbf61fc5ab202951f28451194a7eb093704b26da11cb0ecd308/debugpy-1.8.22-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:c1ffb9953708b648ecf6acd2e5ad2c002bf68785ed618197dd4463a2a7226f39", size = 3994351, upload-time = "2026-09-15T21:44:27.31Z" }, + { url = "https://files.pythonhosted.org/packages/4f/11/2e34a4f21d6155b0c58003fd6fca9819df7d6fe7d7acd83b146b648a57ee/debugpy-1.8.22-cp313-cp313-win32.whl", hash = "sha256:ba810a66b437e3c43ca0ce0892404f010338286263ca8e5108442d76a9485337", size = 5359071, upload-time = "2026-09-15T21:44:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f5/c2b3e8260388d7b76bc4e5a3a29648a0ca47dca6a2721d6549ff2d944eee/debugpy-1.8.22-cp313-cp313-win_amd64.whl", hash = "sha256:f49b1d6cecf326b63c06d7f517e4a6642777f758c58cb799e7bc5a6dd74721c2", size = 5400362, upload-time = "2026-09-15T21:44:30.857Z" }, + { url = "https://files.pythonhosted.org/packages/ea/95/d97240bb68b86db8d0a07602be068aae9e50f17063d696a4c6f619975311/debugpy-1.8.22-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d", size = 2493148, upload-time = "2026-09-15T21:44:32.518Z" }, + { url = "https://files.pythonhosted.org/packages/70/9e/994fe1fb23dd4a5a8d059a0dc95da4b0821907200a61c2d5a4fdc21b6cf8/debugpy-1.8.22-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9", size = 3961025, upload-time = "2026-09-15T21:44:34.387Z" }, + { url = "https://files.pythonhosted.org/packages/37/e0/66319918883cf6dd6dea3805210c3cafd9e65dfaa6f8b852482cff10a026/debugpy-1.8.22-cp314-cp314-win32.whl", hash = "sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294", size = 5359408, upload-time = "2026-09-15T21:44:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/94/53/5b4d91f7fffb9393ac907133201d1da0c070d57e463ee8e2ec1db3cb63b6/debugpy-1.8.22-cp314-cp314-win_amd64.whl", hash = "sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e", size = 5397429, upload-time = "2026-09-15T21:44:38.429Z" }, + { url = "https://files.pythonhosted.org/packages/24/71/580df4aed96c9b23c9cbb049907d8ac1347050afdb8f450df20649d51578/debugpy-1.8.22-cp315-cp315-macosx_15_0_universal2.whl", hash = "sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07", size = 2493380, upload-time = "2026-09-15T21:44:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/10/a323945b0966e560c40fe48470d2847bfc7692a8f550d7086e9a28f5b623/debugpy-1.8.22-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5", size = 3975456, upload-time = "2026-09-15T21:44:41.623Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f9/4383635281999c3eaa44bf327de523ba42240b35eb87c1130adac4a829c6/debugpy-1.8.22-cp315-cp315-win32.whl", hash = "sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0", size = 5359469, upload-time = "2026-09-15T21:44:43.234Z" }, + { url = "https://files.pythonhosted.org/packages/7b/14/4fe9f46a9c825c8cbc6dc4aebc2a885b0bceb37ce2a37699c170482e7a40/debugpy-1.8.22-cp315-cp315-win_amd64.whl", hash = "sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a", size = 5397869, upload-time = "2026-09-15T21:44:44.783Z" }, + { url = "https://files.pythonhosted.org/packages/56/3d/c7dc9f35bc9e22cdb53679f844bee40fd5cff871862f60595869a433400d/debugpy-1.8.22-py2.py3-none-any.whl", hash = "sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947", size = 5374857, upload-time = "2026-09-15T21:44:54.454Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -836,7 +1001,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1016,14 +1181,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.52" +version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/fd/df0bafa4eb5ea2f51e1adee9f7a94c8e62c5d180e65117045dfca3439c8a/gitpython-3.1.52.tar.gz", hash = "sha256:de0a8ad86274c6e75ae8b37dd055ba68f19818c813108642263227b20775b48e", size = 223726, upload-time = "2026-07-16T03:15:59.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/ff/fee293f1ec3e1b64db6eb270e00964f29c086d769201474cedb8d1608930/gitpython-3.2.1.tar.gz", hash = "sha256:0f1229ec5c057bdabdeeda8bda4f17bda679ab33dbccc00f24e6a480553c8613", size = 241688, upload-time = "2026-10-14T12:20:20.589Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/90/04dff7c1e176bb1c3011ef1647393d368790da710d8dde1cdcfad301f45a/gitpython-3.1.52-py3-none-any.whl", hash = "sha256:79a36ee1f83523214a3f72d56cf1c4e490d577dc61af77e43dfe5862bd9da01a", size = 215366, upload-time = "2026-07-16T03:15:58.239Z" }, + { url = "https://files.pythonhosted.org/packages/6b/50/2a923c4dc02522ff9567f07c67ed1deca6d5fa9474f4708168b8088dfa37/gitpython-3.2.1-py3-none-any.whl", hash = "sha256:d66b0832aa8755c1a84892937282df0befb16c97e5a64aeebd68be9c5e323f53", size = 231548, upload-time = "2026-10-14T12:20:18.844Z" }, ] [[package]] @@ -1122,11 +1287,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] @@ -1249,6 +1414,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, ] +[[package]] +name = "legacy-cgi" +version = "2.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/9c/91c7d2c5ebbdf0a1a510bfa0ddeaa2fbb5b78677df5ac0a0aa51cf7125b0/legacy_cgi-2.6.4.tar.gz", hash = "sha256:abb9dfc7835772f7c9317977c63253fd22a7484b5c9bbcdca60a29dcce97c577", size = 24603, upload-time = "2025-10-27T05:20:05.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/7e/e7394eeb49a41cc514b3eb49020223666cbf40d86f5721c2f07871e6d84a/legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd", size = 20035, upload-time = "2025-10-27T05:20:04.289Z" }, +] + [[package]] name = "librt" version = "0.13.0" @@ -1345,6 +1519,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1430,6 +1616,15 @@ wheels = [ { 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 = "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 = "mergedeep" version = "1.3.4" @@ -1441,14 +1636,14 @@ wheels = [ [[package]] name = "mistune" -version = "3.3.3" +version = "3.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/a5/2dab368d6950e6808904dec98f54c7e726ee7be4a0c6afe00e6e011bd52d/mistune-3.3.3.tar.gz", hash = "sha256:c4c6c0c840b8637a2e9b8b6d607eb7c8f00888bf14c754409bcd339e848c2477", size = 115363, upload-time = "2026-07-09T06:18:05.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/92/328a294a6de83bacb95bed01f04e0eaff4e3616ee359fc821a5dfc539b02/mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe", size = 121426, upload-time = "2026-07-22T05:22:30.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/70/b1e4737b84163db5bb1dfde6f216dbfbf32783330a9989c965e121172830/mistune-3.3.3-py3-none-any.whl", hash = "sha256:99de1585e42dcbd826faa9e11a202727a5e202e4e4722a4c69ac1ff615793dd7", size = 63569, upload-time = "2026-07-09T06:18:03.839Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862, upload-time = "2026-07-22T05:22:29.419Z" }, ] [[package]] @@ -1568,140 +1763,183 @@ wheels = [ [[package]] name = "multidict" -version = "6.7.1" +version = "6.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, - { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, - { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, - { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, - { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, - { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, - { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, - { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, - { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, - { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, - { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, - { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, - { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, - { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, - { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, - { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, - { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d6/99/1d4d69c3512d0ddbfa3a1b69cfd9a151012ab2eb4eabbb096201b1f0b7d8/multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2", size = 182404, upload-time = "2026-09-21T17:59:05.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/1c/fb2114207afc7f00ab9897ae5beeee27de5ec58ea8b9623e879f2c56f003/multidict-6.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8", size = 97307, upload-time = "2026-09-21T17:54:14.957Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/1ab8c7349adbb30c8cf41b88b0794740df8ea9f7de2e382da040a2f5bd8e/multidict-6.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19", size = 58830, upload-time = "2026-09-21T17:54:16.261Z" }, + { url = "https://files.pythonhosted.org/packages/e0/89/b845535014a9ad50537ca50204aaf6351c5368822a3acd9da854fb2a5a34/multidict-6.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa", size = 57645, upload-time = "2026-09-21T17:54:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4f/28ebb40a6f8e8aa7be78bcbe4bf9a1ac9518c40ef88accf2d4cd6ed7a7e0/multidict-6.9.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299", size = 301535, upload-time = "2026-09-21T17:54:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/33/51/0de5fc980df34d1ffed9ef68f60ce86c2d4baa3d422598a53f8730c56757/multidict-6.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96", size = 298796, upload-time = "2026-09-21T17:54:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/dd86067bdd2e961fdee8ec659fabc50f4e76403616b508714ec2686b0a49/multidict-6.9.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188", size = 282392, upload-time = "2026-09-21T17:54:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/2f/52/5464a2675579e3adb48b7cd4707a59da499660f0e22c024bc7281d2d9bdf/multidict-6.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a", size = 314538, upload-time = "2026-09-21T17:54:23.366Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/7f2b3fafe064181669157ce872a097b08a0546fb6fd2481e8c5604b47ec3/multidict-6.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e", size = 315675, upload-time = "2026-09-21T17:54:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/bd/53/f65ed68b96eea2b4f6fa4f9031bdfccd8f3097c429e3d371c545d1c40ca4/multidict-6.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab", size = 303685, upload-time = "2026-09-21T17:54:26.236Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5f/ade05795c6fb9adfccd61695a0f0509bc793583f589c4b1b87f27215a139/multidict-6.9.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88", size = 280306, upload-time = "2026-09-21T17:54:27.685Z" }, + { url = "https://files.pythonhosted.org/packages/60/b2/c996349a61e0f43c7c95877943a67476261bf906ff993670afbd33ff5bf4/multidict-6.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1", size = 291691, upload-time = "2026-09-21T17:54:29.004Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/8b7016d1084854729c80acfae71c5a51ea7e7c9411d66c3e2729772f5723/multidict-6.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c", size = 290357, upload-time = "2026-09-21T17:54:30.491Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/801653b6aa9343a7430cecdefa1bf71ff24432224c02f15cc15e47837652/multidict-6.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126", size = 305831, upload-time = "2026-09-21T17:54:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f3/9fc997861920bdbf70db82bdce5c802ec6477754c4bd78cff2898e3c63f9/multidict-6.9.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410", size = 308995, upload-time = "2026-09-21T17:54:33.571Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5f/278b5a2c99d6de27914b49158dd6f2a6481e53359142cc366dff04821a2f/multidict-6.9.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5", size = 279240, upload-time = "2026-09-21T17:54:35.025Z" }, + { url = "https://files.pythonhosted.org/packages/3d/db/0922594c601a691eb4365a83f4f92104ef87a9728d8066fa25c99349e754/multidict-6.9.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4", size = 305487, upload-time = "2026-09-21T17:54:36.555Z" }, + { url = "https://files.pythonhosted.org/packages/64/cb/07bca86d4a361a87128f3019540d7689800fdcad44e3b3ad08f1f035ed74/multidict-6.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c", size = 301997, upload-time = "2026-09-21T17:54:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e3/99591bf626613ea35779d2460949b45e59e589dc9058d6693c923eccd4e5/multidict-6.9.1-cp310-cp310-win32.whl", hash = "sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9", size = 51205, upload-time = "2026-09-21T17:54:39.152Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/10c008aac77297aa618631e8726ece31668f9dc9d9c848e4b5d2e68eb3d3/multidict-6.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0", size = 59049, upload-time = "2026-09-21T17:54:40.465Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/da73a58bef2613571902a824f1e11185207ba756215c99038f6d220bc167/multidict-6.9.1-cp310-cp310-win_arm64.whl", hash = "sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2", size = 54610, upload-time = "2026-09-21T17:54:41.605Z" }, + { url = "https://files.pythonhosted.org/packages/e3/24/3823efc330630a1f132bcd0a9b182ddec3c53f452f551c8e8d3aaf5a2d20/multidict-6.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4", size = 96739, upload-time = "2026-09-21T17:54:42.815Z" }, + { url = "https://files.pythonhosted.org/packages/6a/69/36331fe1d3aeb0c525a9972d6cf82791075a395aa96f026f060009d691ae/multidict-6.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987", size = 58514, upload-time = "2026-09-21T17:54:44.064Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ca/5860651f782078ef13f2761c2593f4ba066d76f626b2f30f23a7f838b775/multidict-6.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c", size = 57303, upload-time = "2026-09-21T17:54:45.304Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9f/c56e2fa223cb5d182660e1a09eec38880f96b7cd22769582f74447c60418/multidict-6.9.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1", size = 320076, upload-time = "2026-09-21T17:54:46.826Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/eda4fe0cad51f96096363ed32d3e0f8df28dab00beff4d92adc9db517726/multidict-6.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07", size = 315489, upload-time = "2026-09-21T17:54:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/14/10b1ded0a4085a51c3054125f854da4f47ce4fec724a2d2503231b83f3db/multidict-6.9.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1", size = 296716, upload-time = "2026-09-21T17:54:49.749Z" }, + { url = "https://files.pythonhosted.org/packages/dc/14/04d155dd18528443cb9009f3a9f35f7417773797d072c95104ff17b8b9b7/multidict-6.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3", size = 330244, upload-time = "2026-09-21T17:54:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/8b/90/284d10b3a9e5b312a8ec5b7a175fc7d560eefdc99886320449891a197992/multidict-6.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80", size = 331288, upload-time = "2026-09-21T17:54:52.595Z" }, + { url = "https://files.pythonhosted.org/packages/27/50/f420de3683f9b047fc5588064d84043ef57a7152ef451b8373dfc7068d22/multidict-6.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387", size = 320663, upload-time = "2026-09-21T17:54:53.91Z" }, + { url = "https://files.pythonhosted.org/packages/01/ab/0120a650d7ce4fe167299c13a13cc4ee2dc72458a9c0e9e9330b824b73c9/multidict-6.9.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345", size = 291139, upload-time = "2026-09-21T17:54:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/c66342f73ee5cd2c9b1dda6cfce89b5f348c48921b8c8b6e59aafbbfe31a/multidict-6.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd", size = 310044, upload-time = "2026-09-21T17:54:56.745Z" }, + { url = "https://files.pythonhosted.org/packages/05/0d/e44b90d9e77e44ec3473c4c7ac7ea95764d7e8fbf6d5a085ef9bff7ce0c2/multidict-6.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f", size = 303279, upload-time = "2026-09-21T17:54:58.178Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/7741f7c23c211fae31ab546f0ba41569dae7671cf7ca30d5afb59e9d92aa/multidict-6.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b", size = 326177, upload-time = "2026-09-21T17:54:59.825Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/0cdbfbbe36316b2ef4b8e011afaca2cf291a73fe4fabf34dd0b22e3ee48a/multidict-6.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82", size = 323526, upload-time = "2026-09-21T17:55:01.43Z" }, + { url = "https://files.pythonhosted.org/packages/98/05/0c86e9678e78f4596df0ed7f59d2aa755107da3f2a92f1b7fa150e94cbb8/multidict-6.9.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953", size = 290278, upload-time = "2026-09-21T17:55:02.993Z" }, + { url = "https://files.pythonhosted.org/packages/95/8e/71bd7f43c4883cba6c5f96db88bd2311d52e74ad7680be5915938490d56f/multidict-6.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a", size = 321142, upload-time = "2026-09-21T17:55:04.48Z" }, + { url = "https://files.pythonhosted.org/packages/2d/00/bf59d6bb22a4c152e4039574c29490caf3c65107715121807b4bd7cde244/multidict-6.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13", size = 319045, upload-time = "2026-09-21T17:55:06.119Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3c/f27f4f045198bf1c4bcd6eb3b6cf06be2dceaceb6e67222b903fce7a6749/multidict-6.9.1-cp311-cp311-win32.whl", hash = "sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f", size = 51309, upload-time = "2026-09-21T17:55:07.531Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/c2064aef64efc007bc89a571f843e5772f27946eb9bc018731bf8016e319/multidict-6.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e", size = 59148, upload-time = "2026-09-21T17:55:08.936Z" }, + { url = "https://files.pythonhosted.org/packages/45/a1/3bab1827edd813dfb90712af5cbb7fc1473ed4d9c871d103df4f4bb950c2/multidict-6.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d", size = 54258, upload-time = "2026-09-21T17:55:10.191Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0d/4b5afb6d3e545c9af0cdfe2db8f6f4c6664568c23d863d888674e447e6a4/multidict-6.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe", size = 98360, upload-time = "2026-09-21T17:55:11.508Z" }, + { url = "https://files.pythonhosted.org/packages/89/ab/1b9ca66251899981b21138b87da9d5a9c2c81af12b1ea7d19466972f7fe2/multidict-6.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006", size = 59979, upload-time = "2026-09-21T17:55:12.866Z" }, + { url = "https://files.pythonhosted.org/packages/36/eb/6ae44062466c26c8469ef43f2481a6a48d8cea0587b2d54514ec92e2adfd/multidict-6.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42", size = 57736, upload-time = "2026-09-21T17:55:14.223Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5c/a67817593019257a4ac8b0d1b4c426030e637c047b0692ef405439ecea7a/multidict-6.9.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5", size = 333097, upload-time = "2026-09-21T17:55:15.591Z" }, + { url = "https://files.pythonhosted.org/packages/90/bf/599ae2e6222822d88a247a8a7ae82fe6fd25d5700757b79603d5edafe6a0/multidict-6.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea", size = 334196, upload-time = "2026-09-21T17:55:17.015Z" }, + { url = "https://files.pythonhosted.org/packages/15/10/d8aac5acacbe7f5c117866c776ec26d5f15a868759b6f37ad8e7ed3b5b02/multidict-6.9.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24", size = 317465, upload-time = "2026-09-21T17:55:18.412Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/714f796f7293a8b1c5c3f465a26996231d5450c54e85d64ef1258c091134/multidict-6.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24", size = 345914, upload-time = "2026-09-21T17:55:20.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b1/e37fbf769c567be277bcf32df6234035a4384677fcc1bd852752be3d6b93/multidict-6.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a", size = 351163, upload-time = "2026-09-21T17:55:22.21Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5b/db08419c1e1f7c9d60cfd2787b2b517d7ae4ebbda8281b48a33eb5141467/multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d", size = 336881, upload-time = "2026-09-21T17:55:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/cc9bc8a62651d2d53ab93ee4993b3a71b7cb78eb8ebfc9c757a5b6698617/multidict-6.9.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805", size = 303405, upload-time = "2026-09-21T17:55:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0c/8e912afafa70e944dbb8bec4b66ca6e008511395278c0d3dd0e89567536a/multidict-6.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02", size = 324265, upload-time = "2026-09-21T17:55:26.989Z" }, + { url = "https://files.pythonhosted.org/packages/66/6a/62c2af80fb085e6234805017af857b8e913dfac7a55e9c1349c27768c58a/multidict-6.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8", size = 322085, upload-time = "2026-09-21T17:55:28.732Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6b/35bf801b336fd960811207203ffdcdc24acc3558b3e7ff2e1c914b141e70/multidict-6.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580", size = 338107, upload-time = "2026-09-21T17:55:30.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/495ef65bf5bba29d142d81b3fe1b8154b919bed70e96491b1e93c3c26f0f/multidict-6.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004", size = 339631, upload-time = "2026-09-21T17:55:32.012Z" }, + { url = "https://files.pythonhosted.org/packages/19/bd/057fdff5f4e04dcd40a960e38f77d19d3c4b67dd243ffa5f43718edb29fc/multidict-6.9.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60", size = 302080, upload-time = "2026-09-21T17:55:33.817Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/9ace933ee8dad808632523726f42255b09600087219e3d4ead7369820910/multidict-6.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a", size = 341107, upload-time = "2026-09-21T17:55:35.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ac/7c1204406097bfc5c283d4a3287d61166807d189917567df4c1318484fc4/multidict-6.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6", size = 333482, upload-time = "2026-09-21T17:55:37.002Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/a70c32ea3299ebe00f44533740cb46905717c51faf29bbd3ce8bb5886d9d/multidict-6.9.1-cp312-cp312-win32.whl", hash = "sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb", size = 52548, upload-time = "2026-09-21T17:55:38.529Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2e/8c9c2591df01e5692ad1bf92febb082a17b1c6c3a19aa8b7ff49988fd989/multidict-6.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203", size = 59622, upload-time = "2026-09-21T17:55:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/d0/95/59f4472ec512bc180fd207899594c7803ece96262ff9aaa3a4b6d7da6940/multidict-6.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc", size = 54930, upload-time = "2026-09-21T17:55:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/ddf7c76860f5f553a23ad3ca38463ccf5247081618977742c8dc4625355e/multidict-6.9.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3", size = 63266, upload-time = "2026-09-21T17:55:42.676Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d3/f4ae5945ea2de597eaeb4eca3a74c59783ace54482c9b49435442b63efa8/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382", size = 55771, upload-time = "2026-09-21T17:55:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/15468239920040d01c686e5ae669e6382f7bf31fc3e5e0a6b8f1c3b32d3b/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a", size = 57116, upload-time = "2026-09-21T17:55:45.315Z" }, + { url = "https://files.pythonhosted.org/packages/17/1b/b958f06aac2d8b1e485eb1c105b1b159cbf3868249e5142142451577dad2/multidict-6.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40", size = 97692, upload-time = "2026-09-21T17:55:46.674Z" }, + { url = "https://files.pythonhosted.org/packages/93/6c/d6cfe18e61010166d7237d7527c775eb9843e7078feadea45b7628751b60/multidict-6.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8", size = 59556, upload-time = "2026-09-21T17:55:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/46/46/9b4c1127cece0289fb207d04aae5116382ddfcb2a4dc8e0cb49a33f3c7b3/multidict-6.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c", size = 57357, upload-time = "2026-09-21T17:55:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fc/c18b07100a6064573e49e538d13d47eb2b5221f48080512df78aef54ab04/multidict-6.9.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a", size = 330433, upload-time = "2026-09-21T17:55:51.435Z" }, + { url = "https://files.pythonhosted.org/packages/62/ff/52a0082adeb69656b634609d4fb0ae65456deaae4cca3d5a12656626dc50/multidict-6.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8", size = 332487, upload-time = "2026-09-21T17:55:52.937Z" }, + { url = "https://files.pythonhosted.org/packages/39/3e/80bd4729635a7af371ff4dcc312594cf96e73f98854491be8740792de430/multidict-6.9.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94", size = 316911, upload-time = "2026-09-21T17:55:54.751Z" }, + { url = "https://files.pythonhosted.org/packages/15/f4/7ea4a907e053e924e2e1694d90560f79f72056504bfd5e8483695f9527d2/multidict-6.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa", size = 344950, upload-time = "2026-09-21T17:55:56.5Z" }, + { url = "https://files.pythonhosted.org/packages/0d/16/9642ae41fbfbdc546aa481c2b07ae1bc087a94ab5f5020b4d9ab77da401f/multidict-6.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5", size = 350264, upload-time = "2026-09-21T17:55:58.073Z" }, + { url = "https://files.pythonhosted.org/packages/93/f2/e06c8e42074d0a4b8419bfe92afbd1d262190b69549ecbcd2dffa4f103c2/multidict-6.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c", size = 336213, upload-time = "2026-09-21T17:55:59.723Z" }, + { url = "https://files.pythonhosted.org/packages/98/43/d7cf9ef4700e7c25244590d1b4f20cf12cbed86beba6c17be4d9e48c1099/multidict-6.9.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b", size = 302555, upload-time = "2026-09-21T17:56:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3a/54209920324bc3928f5f2ba28b01a6dd957a9d48c3aaff77e38faaf5668d/multidict-6.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05", size = 322247, upload-time = "2026-09-21T17:56:03.265Z" }, + { url = "https://files.pythonhosted.org/packages/33/50/df96f961b178b621ab0adbf9221d4fd21534e1064fd9f5e85c1fa27fea55/multidict-6.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a", size = 321787, upload-time = "2026-09-21T17:56:04.894Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9b/37f354562a8f82f9c1f63d3a94300fc82126d50320175f0702bbd544f87c/multidict-6.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418", size = 335498, upload-time = "2026-09-21T17:56:06.783Z" }, + { url = "https://files.pythonhosted.org/packages/1e/44/78e366efc6c004185295cc9eb9711c10f13eadf3b547603b9c5526329976/multidict-6.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1", size = 338244, upload-time = "2026-09-21T17:56:09.162Z" }, + { url = "https://files.pythonhosted.org/packages/ad/2b/ab5bd3964691abe4d14b43bdbdb8a621b77cea2ca352dc93159ec0a4575b/multidict-6.9.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721", size = 301641, upload-time = "2026-09-21T17:56:11.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/f6bc899f5aafaba755edc8e4bb934bc00b1e405f8d3fbc1dd10283738074/multidict-6.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771", size = 340162, upload-time = "2026-09-21T17:56:13.076Z" }, + { url = "https://files.pythonhosted.org/packages/eb/03/a8fc809ef364b8c231c065ea2ea2f97ae86d4f9a0e36b5721759fd606778/multidict-6.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14", size = 332826, upload-time = "2026-09-21T17:56:15.175Z" }, + { url = "https://files.pythonhosted.org/packages/f2/27/32dde80245e2024e9bb7a6ca3c2fb7c695dc74014cba557cc35be6f91e24/multidict-6.9.1-cp313-cp313-win32.whl", hash = "sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40", size = 52378, upload-time = "2026-09-21T17:56:16.959Z" }, + { url = "https://files.pythonhosted.org/packages/be/78/1bac2987edac273a6b27fa50e9204bc3466c94a7b4f6a44828d730f8810f/multidict-6.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d", size = 59449, upload-time = "2026-09-21T17:56:18.336Z" }, + { url = "https://files.pythonhosted.org/packages/e1/32/2a77ce19eea48cb9b3521a202b513ba3349aafa702c8317be0b645ffb74b/multidict-6.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4", size = 54713, upload-time = "2026-09-21T17:56:19.774Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/c6041015aa2cdc11e1dff0cae58898ae5525d19e35efd65a4ecf3c6ea235/multidict-6.9.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5", size = 62336, upload-time = "2026-09-21T17:56:21.181Z" }, + { url = "https://files.pythonhosted.org/packages/23/db/95bd0afb130a0149c955f1f066143528d60864e64f41ff5a1e1313609361/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9", size = 54892, upload-time = "2026-09-21T17:56:22.63Z" }, + { url = "https://files.pythonhosted.org/packages/94/cc/8aa34ae8d09498e8da003f485c03371b55cf1bc6d56cff861b3a7de61251/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca", size = 56251, upload-time = "2026-09-21T17:56:24.36Z" }, + { url = "https://files.pythonhosted.org/packages/52/48/22baa3b95375096369b04c4348af14139570f302f951773d2c68bf0ccf2e/multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840", size = 96345, upload-time = "2026-09-21T17:56:25.777Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3e/b96779dcac28ec6d491fd821112a0156b519b6701bba186cdf6f6df737f4/multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800", size = 58901, upload-time = "2026-09-21T17:56:27.364Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/8e6d950f02ca6ecc00ecf671f2ddb5dab7017671a8d197326f75621f5786/multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d", size = 56491, upload-time = "2026-09-21T17:56:28.901Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8f/212ed7e03282c7bc271e3796bf985b53d5727c2827361e3dd9c5ee6b093f/multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201", size = 332297, upload-time = "2026-09-21T17:56:30.495Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/555aab000e03ecba85c0a1837fd5674d8bf10949490df7800da3a40660ce/multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701", size = 329197, upload-time = "2026-09-21T17:56:32.652Z" }, + { url = "https://files.pythonhosted.org/packages/3d/25/f3539b5bdc147beba66015795dfe589eedcc580348fb1a8c69b1df4196b5/multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610", size = 308456, upload-time = "2026-09-21T17:56:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7d/4665cad8fc326787d218879be879d86dfe32f006c1f88ae47b04d3b52fd5/multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694", size = 343382, upload-time = "2026-09-21T17:56:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/41/01/e1e9abe27f492b22dede492bc423015597412b14ce074f7475f4ecd80185/multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a", size = 346848, upload-time = "2026-09-21T17:56:38.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/8d54118bc228e64e1087f1729647b93bbea225eda4a3f914663a6f410bca/multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a", size = 333612, upload-time = "2026-09-21T17:56:40.041Z" }, + { url = "https://files.pythonhosted.org/packages/76/e1/9509dafc1fc68948e75842a74533ddfa64af57f2b17810e9cacfa68a829b/multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba", size = 304246, upload-time = "2026-09-21T17:56:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/96b93808e1bec0b51d49d89859e49b82a94f7b2e58eca0fc44ffcafd3ae5/multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290", size = 321983, upload-time = "2026-09-21T17:56:44.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1c/08e5184df026b9d8e05a6dce5da22628e46bc4a7c7f03b8bf3c6939a8c46/multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d", size = 315927, upload-time = "2026-09-21T17:56:46.208Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5d/9ff39e7387f6fcc9a81af5dbf055fde08149849a04f8df10ca5b086b0d39/multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828", size = 336827, upload-time = "2026-09-21T17:56:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/30fbb2cab22203128928066de94be247dc9a3230bf2bfc54437ac3b5f9f7/multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132", size = 336317, upload-time = "2026-09-21T17:56:49.778Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/2480aef6b7d8e7ab89b064ae14f12ec069570dab3a5c13e0155912293247/multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b", size = 303258, upload-time = "2026-09-21T17:56:51.829Z" }, + { url = "https://files.pythonhosted.org/packages/4c/04/dbe59cf4ea3778600e8e70e30b31aa9118ad6da2844bc9f717804cbaa000/multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d", size = 337232, upload-time = "2026-09-21T17:56:53.537Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8c/abe55548f06f12878588f08eb803c5a391283f01243f784cb00adac79231/multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194", size = 330261, upload-time = "2026-09-21T17:56:55.335Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/ba67f668478f152bdfa18abe72e7588bd078bf8b42c172fcf542fcf45826/multidict-6.9.1-cp314-cp314-win32.whl", hash = "sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841", size = 51715, upload-time = "2026-09-21T17:56:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/014bb2128aedc8157d2848f0ec8a0209c6b822fb88deea012834e1c4a4e8/multidict-6.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3", size = 58621, upload-time = "2026-09-21T17:56:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/66/34/8f43c03c2d8821a2320b8c560d8a49d0be2920627edfac3f546922f91969/multidict-6.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9", size = 53959, upload-time = "2026-09-21T17:57:00.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/4f2a5eeea10b6d42634ba168ec6efc732ef218772e57b8239cd485c202f6/multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76", size = 107694, upload-time = "2026-09-21T17:57:01.785Z" }, + { url = "https://files.pythonhosted.org/packages/32/ce/c68d08ac2096c1528ff20d77fd10f7e401db3e534ac5a1c7726e36a77c2f/multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b", size = 64766, upload-time = "2026-09-21T17:57:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9a/9f5c270c88fe289b4876fe28f9932a70e015a7916aff1e5b0325f7597b11/multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485", size = 62723, upload-time = "2026-09-21T17:57:05.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/63/c9a0b131354dd949426166cef712d708f436a8cf261114f8d5d2aec440ca/multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455", size = 290445, upload-time = "2026-09-21T17:57:06.687Z" }, + { url = "https://files.pythonhosted.org/packages/9d/41/ed27d1d20ba91f4e13e8f7dcfaa592614ae0e1d5f45e03171d23c3b098ac/multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125", size = 304375, upload-time = "2026-09-21T17:57:08.717Z" }, + { url = "https://files.pythonhosted.org/packages/66/6f/a7b26167173c3b73d6e5ebb0856c5154ad005156c3a2fa4062329921819c/multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390", size = 281746, upload-time = "2026-09-21T17:57:10.713Z" }, + { url = "https://files.pythonhosted.org/packages/34/1d/da27bdd49b0cb84263288f5f48dbd915f2d17fd228fee2078ca607e80277/multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9", size = 311499, upload-time = "2026-09-21T17:57:12.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/36/ab0299a7abf53e38ff75d96c64480f1e2ff8edde985f93f7a1ad5e08bd94/multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94", size = 313553, upload-time = "2026-09-21T17:57:14.548Z" }, + { url = "https://files.pythonhosted.org/packages/5a/44/e21be702efc9c9ba097c8be874c68860b1cb6cf9c85611263039bf95b37e/multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45", size = 299898, upload-time = "2026-09-21T17:57:16.192Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e93bc0dc947c7f872e502cfaa7ad9d98f001e1c93cfa6f4940457a6e187a/multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8", size = 273054, upload-time = "2026-09-21T17:57:17.793Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c9/945b44493fbe7af26ffe1e43c506b376daeab070fc1a0923bef9bbf572eb/multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3", size = 294751, upload-time = "2026-09-21T17:57:19.631Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/0969a9b38b814e5fa3af1b0a57708c49b3d9b3d55c208bac6e98b87bb028/multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f", size = 284523, upload-time = "2026-09-21T17:57:21.313Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1e/c32ddf53234c228b1a374ddabca19f542ac7f32752f5155982abe2304833/multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709", size = 290275, upload-time = "2026-09-21T17:57:23.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/09/7a99a3daafd987330abb865a2d36e432fd8bf18e790ddda55c5de15eb98f/multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983", size = 303350, upload-time = "2026-09-21T17:57:25.086Z" }, + { url = "https://files.pythonhosted.org/packages/2e/09/7e6ebaecc4e226d7fe27973c271d58b75c14c24e84c23736d21dc39997dd/multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6", size = 272232, upload-time = "2026-09-21T17:57:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6e/420e9e879b21cbdb5036214ee238efabcfab7bf262d3fc124132050f79ff/multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b", size = 300525, upload-time = "2026-09-21T17:57:29.004Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/c6b0896850b3ceb190ca302760d63148843371f027c5dde3d1d732343a08/multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e", size = 295933, upload-time = "2026-09-21T17:57:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/f0ff3384a756227741c72f4ee94cadf1f9900df0ae961e48639f96115b1c/multidict-6.9.1-cp314-cp314t-win32.whl", hash = "sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd", size = 57776, upload-time = "2026-09-21T17:57:32.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/87/013e1eed30ad46bec1eb68947bc9caf55ba2fd0d6aed04fe964e3b44c23b/multidict-6.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9", size = 64881, upload-time = "2026-09-21T17:57:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/7f/95/64dd049bb4c53426e2fee7afb874daaa8b4426c41878785883a94511ecf8/multidict-6.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3", size = 58582, upload-time = "2026-09-21T17:57:36.19Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2c/26bc1723592cfe965b575652a7749026474b0625b04b04564e1fba265dc5/multidict-6.9.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1", size = 62339, upload-time = "2026-09-21T17:57:37.967Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c5/614d2eb9995232d066aae7f17d1d454060b6b5d882b22fbaf6150b05ef1d/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82", size = 55009, upload-time = "2026-09-21T17:57:39.746Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/f7179ecdf9cdd3581ace3292c3b883a1a500a374aa4e619bf6eb521c62e5/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea", size = 56273, upload-time = "2026-09-21T17:57:41.544Z" }, + { url = "https://files.pythonhosted.org/packages/ac/72/c9dffcecccd1f8b9a58f960ec4eaecab62c4edb08d5bb984d4de67fe3fdb/multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c", size = 96390, upload-time = "2026-09-21T17:57:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3f/76542360e655cecdb7c158c7af88c69c4db0d2aac3421659d41214927229/multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08", size = 58900, upload-time = "2026-09-21T17:57:45.105Z" }, + { url = "https://files.pythonhosted.org/packages/04/15/f28c71af461dd4ff15653a165cfd8ddaf01f3387ff80d2dd0de057d527d0/multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b", size = 56540, upload-time = "2026-09-21T17:57:46.913Z" }, + { url = "https://files.pythonhosted.org/packages/19/de/5ee4050c3ddad88c605a89fc1cd18e89abd40a64f183e6fb5276b6eb7f52/multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919", size = 331818, upload-time = "2026-09-21T17:57:48.825Z" }, + { url = "https://files.pythonhosted.org/packages/70/e2/2d4e3c3ee70f583334ff236fe6a3babf7a05ce56169a15df65453af647c0/multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519", size = 329800, upload-time = "2026-09-21T17:57:50.91Z" }, + { url = "https://files.pythonhosted.org/packages/d6/14/f64bd05667343a7250cdd99f81bda2270e3959f00832441f10c7f904e1c1/multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9", size = 312048, upload-time = "2026-09-21T17:57:53.053Z" }, + { url = "https://files.pythonhosted.org/packages/64/bf/a91256ce00199e7544e3eda79c4e9544eb7388c38fa22f11638c2e357ccb/multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d", size = 343676, upload-time = "2026-09-21T17:57:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/628b978413518159228c2b2ec38645362ceb6427e935d83d716e26490a49/multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67", size = 347028, upload-time = "2026-09-21T17:57:57.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/5f9dc2b5ac7dd7a82458ae9cd283b99c185834f23442ace295362dcc5e45/multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e", size = 335670, upload-time = "2026-09-21T17:57:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6f319ee000f5ab3fa0055b194ad706fddd552c60c76a21d581c87db1d46c/multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca", size = 307804, upload-time = "2026-09-21T17:58:01.2Z" }, + { url = "https://files.pythonhosted.org/packages/3f/24/61be5fe1616d033532e8d63dc789c0f35b9793917834713eefe050589ba9/multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac", size = 323409, upload-time = "2026-09-21T17:58:03.383Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/eab8aaf59892169a0f4e8e78a5516a6caa6abd8e96e6410111258f4ef0ef/multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e", size = 315746, upload-time = "2026-09-21T17:58:05.372Z" }, + { url = "https://files.pythonhosted.org/packages/42/b1/d1e14bab80a74babf6e2ac971a878b84fdcf3a972f3ec3400ca12260fac5/multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e", size = 334529, upload-time = "2026-09-21T17:58:07.35Z" }, + { url = "https://files.pythonhosted.org/packages/f5/50/a05b1d36f6d3363a5a334cebbf11624c9c7a69d66fd23345eb53b1d43b7e/multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c", size = 336455, upload-time = "2026-09-21T17:58:09.112Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0d/bf33cb097380252b08e1177b5ae0cab822c47ce80d236f3f724953c76721/multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f", size = 305865, upload-time = "2026-09-21T17:58:10.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/a2bd133391204b1cec5a5caf327dc9a06710439b7403220860a8fb164560/multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4", size = 337531, upload-time = "2026-09-21T17:58:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/c0972486f81a06c3bbb23063c85afaac53c51608f7e35bcb101a1006408b/multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d", size = 332464, upload-time = "2026-09-21T17:58:14.673Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/5cbbe852b45d8a7f0ab3ed042386e4894e463010eb230603596cb15d0fee/multidict-6.9.1-cp315-cp315-win32.whl", hash = "sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d", size = 51714, upload-time = "2026-09-21T17:58:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/1fa118386deeae4e7a48870fa5cb073e748b0ac5265a6990651cefc48068/multidict-6.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f", size = 58619, upload-time = "2026-09-21T17:58:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ee/51c45748acfce9cb69d89dcfcd09c874b385407fd6993e885d6905659865/multidict-6.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f", size = 53969, upload-time = "2026-09-21T17:58:20.102Z" }, + { url = "https://files.pythonhosted.org/packages/aa/57/89d2d3dedfae2558853c1770c7cbcdfd5211eb4c4090b1e8cb570544a490/multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7", size = 107679, upload-time = "2026-09-21T17:58:21.98Z" }, + { url = "https://files.pythonhosted.org/packages/c8/df/0c2a28870181c1762f90952b897f6db46c934ce073efe0442604cbc276db/multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68", size = 64753, upload-time = "2026-09-21T17:58:23.622Z" }, + { url = "https://files.pythonhosted.org/packages/43/7e/9c6a3e7619459407d8958f7e56ab3dda7f0738f5aed9bb4ad48076e8de83/multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64", size = 62732, upload-time = "2026-09-21T17:58:25.46Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/fd56c9d83ba0cdc3cf66f9acb278b24ef1f043b620b96d145685a3b57338/multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce", size = 288648, upload-time = "2026-09-21T17:58:27.115Z" }, + { url = "https://files.pythonhosted.org/packages/df/ef/51e79c2442b0f56ac4ac580108d713078b31297e028d468fc220971a305d/multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5", size = 304919, upload-time = "2026-09-21T17:58:29.143Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e4/774700cc5739353fe88a7204ed30e78a9403d98d4bb2496cc91f096d2e71/multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2", size = 289094, upload-time = "2026-09-21T17:58:30.938Z" }, + { url = "https://files.pythonhosted.org/packages/ca/41/0bec327bc3a8271825376b6d8a469d1f80e3ba1e2e730e019836e29663c3/multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1", size = 312007, upload-time = "2026-09-21T17:58:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/9906bf5f1fe20e8e755affccc1a69ecd8aebb079ac65386e5ffc8eefbe75/multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659", size = 314657, upload-time = "2026-09-21T17:58:35.285Z" }, + { url = "https://files.pythonhosted.org/packages/33/92/cf6d665a45663bbe216420fe632da10e9f4f842b07ce02bc3ddf8eb882b8/multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20", size = 301563, upload-time = "2026-09-21T17:58:37.472Z" }, + { url = "https://files.pythonhosted.org/packages/97/4c/9f1b7945526905d58b4b2d422e2ea81326dda71f9bdc9c32c8002639234a/multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471", size = 278540, upload-time = "2026-09-21T17:58:39.551Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d9/133aa472fc1be181de29a0c2e1c4cd778280882f532b404395f646742e46/multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879", size = 294858, upload-time = "2026-09-21T17:58:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/bd/38/21f2305dd20ac5038fa581b5d7e0d8dbbd70dc6b50231c22ec173dacddda/multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3", size = 285461, upload-time = "2026-09-21T17:58:43.844Z" }, + { url = "https://files.pythonhosted.org/packages/ad/21/465a43c214d1d7e2d0c974e727d4b5935f8a0868838b5df9d0984c83c1dc/multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df", size = 288964, upload-time = "2026-09-21T17:58:46.16Z" }, + { url = "https://files.pythonhosted.org/packages/2d/58/72a3e8d56c1e05146d2d1841dbaacf8986375eded1e5a58d30eef191a580/multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c", size = 303924, upload-time = "2026-09-21T17:58:48.395Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b9/911084c04530a682a54536b4f738d3992a593b5ff6fea1d6a1f02f3acc6b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c", size = 278479, upload-time = "2026-09-21T17:58:50.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/eb/9a333346c02c928a9e82534638bce85697c10c46b4901fcd70af19357356/multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d", size = 301650, upload-time = "2026-09-21T17:58:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/67/2f/1d936c4e45c8199db6082b6836e9182e31aa393de0de652637790831ca4b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1", size = 298024, upload-time = "2026-09-21T17:58:55.03Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0c/ca62ae2882b89a4cfeb0bbca873a4afdb5fc32994c4f8a37a7cb52ede7a0/multidict-6.9.1-cp315-cp315t-win32.whl", hash = "sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1", size = 57810, upload-time = "2026-09-21T17:58:57.141Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/b2c2805c3eb6fa4cdf5512fb95ac6174dde71e34dd601c5be52f4df5dc82/multidict-6.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec", size = 64984, upload-time = "2026-09-21T17:58:59.446Z" }, + { url = "https://files.pythonhosted.org/packages/fd/34/03f1d204d698c408f4754c969882609b6b7a902c0e3d16a7033f219cdeda/multidict-6.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3", size = 58742, upload-time = "2026-09-21T17:59:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/be/59/e26cb779be4c591d1a910f59d29aca9fba4de70349840a833beba2652371/multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b", size = 19176, upload-time = "2026-09-21T17:59:03.501Z" }, ] [[package]] @@ -2253,15 +2491,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "11.0.1" +version = "12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/fe/ac50ee24a80a1b344e643f6fb626063cc7041b978cd1dd9a7f9963ae1cd1/pymdown_extensions-12.3.tar.gz", hash = "sha256:a7f2a4e0bc60c6e6babfa7922cfb3b01cd640eb00fb208f3c582e12bae00ea67", size = 870532, upload-time = "2026-10-14T22:40:43.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d0/758afe732c2da502c051b4d03ff88daa3c0fc1a1e671e6c5a6318177bab4/pymdown_extensions-12.3-py3-none-any.whl", hash = "sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74", size = 277597, upload-time = "2026-10-14T22:40:41.543Z" }, ] [[package]] @@ -2300,6 +2538,20 @@ 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-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + [[package]] name = "pytest-dotenv" version = "0.5.2" @@ -2327,11 +2579,23 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, +] + +[[package]] +name = "python-slugify" +version = "9.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/e8/26b1af09d728d604dc16427a39f53985b22a170dbd61addac3f48db73f03/python_slugify-9.1.3.tar.gz", hash = "sha256:90e997f2e0987239ce95e12f700086eb18e1d1d3ee22624fbbdbd095afca42b6", size = 66093, upload-time = "2026-10-07T01:06:20.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/67/5598c98725332a6a4084c7b622d5a1ebe7d6745c1d2f434c9e4e89e04cba/python_slugify-9.1.3-py3-none-any.whl", hash = "sha256:9aced0670e54c5603e2335c0fa9a4011ad4bc41ecb3c46e8097294c46fd60ed4", size = 16032, upload-time = "2026-10-07T01:06:18.891Z" }, ] [[package]] @@ -2497,6 +2761,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "semver" version = "3.0.4" @@ -2574,6 +2851,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + [[package]] name = "tinycss2" version = "1.5.1" @@ -2801,13 +3087,22 @@ 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 = "2026.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/68/f1b440335057bfce71b6e50a9d09445aa2ecbd08359a337976627b8409e7/tzdata-2026.5.tar.gz", hash = "sha256:8cc73c0a0bfca7dbfa59235d60b2eff82231dee33f53d206db1acd9173cfc0a7", size = 200404, upload-time = "2026-10-03T09:23:14.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/21/1e5995a1c920cce14e4bffae20c665ec10e7ed03ab25e006cd741092b718/tzdata-2026.5-py2.py3-none-any.whl", hash = "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac", size = 347996, upload-time = "2026-10-03T09:23:12.535Z" }, +] + [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.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" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] @@ -2969,116 +3264,150 @@ wheels = [ [[package]] name = "yarl" -version = "1.24.2" +version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/df/f1c7a3de0831cd83194f1a85c5bb431b13f81e6b45079314c86d1c4ef3f2/yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12", size = 129057, upload-time = "2026-05-19T21:27:47.564Z" }, - { url = "https://files.pythonhosted.org/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0", size = 91545, upload-time = "2026-05-19T21:27:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75", size = 91380, upload-time = "2026-05-19T21:27:51.953Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1b/8bafab7db23b0567ae9db749099b329d91e3b82bc6028b2050ba583e116c/yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727", size = 105957, upload-time = "2026-05-19T21:27:53.98Z" }, - { url = "https://files.pythonhosted.org/packages/7f/77/21030c2f8d21d21559719beafc772ada2014be933418ed1eaed9cc800e42/yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413", size = 97242, upload-time = "2026-05-19T21:27:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/50/d8/f9ea63d1b6aa910a866e089d871fff6cbd49caab29b86b35221a62dfa0d5/yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9", size = 114719, upload-time = "2026-05-19T21:27:58.037Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a3/04e0ee98ac58a249ea7ed75223f5f901ba81a834f0b4921b58e5cec11757/yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2", size = 112140, upload-time = "2026-05-19T21:27:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90", size = 106721, upload-time = "2026-05-19T21:28:02.586Z" }, - { url = "https://files.pythonhosted.org/packages/65/e7/a52478ebfc66ec989e085c6ae038b9f1bfa4190baa193b133b669c709e2f/yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643", size = 106478, upload-time = "2026-05-19T21:28:04.523Z" }, - { url = "https://files.pythonhosted.org/packages/04/d8/5508530fea8472542de00013ae280765fc938ee196fc4030c43a498afb36/yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac", size = 105423, upload-time = "2026-05-19T21:28:06.515Z" }, - { url = "https://files.pythonhosted.org/packages/84/f1/ece28505e9628e8b756e11bb4f28864a17cc33b6b44db4d2aaf0622bf630/yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f", size = 99878, upload-time = "2026-05-19T21:28:08.637Z" }, - { url = "https://files.pythonhosted.org/packages/3f/52/fb5d34529b46dd84013afcfb30b8d2bc2832ed03d412736f577d604fa393/yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36", size = 114025, upload-time = "2026-05-19T21:28:10.64Z" }, - { url = "https://files.pythonhosted.org/packages/43/f0/ff9d31aaab024f7a251c0ed308a98ae29bf9f7dc344e78f28b1322431ca2/yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a", size = 105613, upload-time = "2026-05-19T21:28:12.784Z" }, - { url = "https://files.pythonhosted.org/packages/31/7d/3296fb3f3ecd52bf9ae6c16b0895c1cda7e9170a2083861552b683f70264/yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53", size = 111665, upload-time = "2026-05-19T21:28:14.393Z" }, - { url = "https://files.pythonhosted.org/packages/1a/74/77aa6ddaca4fbf42e45e675a465c43956dd40702281049975a2aa04eae59/yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342", size = 106914, upload-time = "2026-05-19T21:28:15.893Z" }, - { url = "https://files.pythonhosted.org/packages/d8/02/7611f22cd1d4ed7373eb7f9ee21fde1046edba2e7c0e514880d760352f48/yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4", size = 92658, upload-time = "2026-05-19T21:28:17.471Z" }, - { url = "https://files.pythonhosted.org/packages/91/00/671d0add79938127292839ae44506ce2f7fe8909c72d5a931864f128fd0b/yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39", size = 87887, upload-time = "2026-05-19T21:28:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971, upload-time = "2026-05-19T21:28:20.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c", size = 91507, upload-time = "2026-05-19T21:28:22.556Z" }, - { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343, upload-time = "2026-05-19T21:28:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704, upload-time = "2026-05-19T21:28:25.872Z" }, - { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281, upload-time = "2026-05-19T21:28:27.352Z" }, - { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020, upload-time = "2026-05-19T21:28:29.458Z" }, - { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450, upload-time = "2026-05-19T21:28:31.262Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384, upload-time = "2026-05-19T21:28:32.856Z" }, - { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153, upload-time = "2026-05-19T21:28:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322, upload-time = "2026-05-19T21:28:36.642Z" }, - { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057, upload-time = "2026-05-19T21:28:38.386Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502, upload-time = "2026-05-19T21:28:40.091Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253, upload-time = "2026-05-19T21:28:41.987Z" }, - { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345, upload-time = "2026-05-19T21:28:43.93Z" }, - { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558, upload-time = "2026-05-19T21:28:45.806Z" }, - { url = "https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45", size = 92808, upload-time = "2026-05-19T21:28:48.465Z" }, - { url = "https://files.pythonhosted.org/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122", size = 87610, upload-time = "2026-05-19T21:28:50.07Z" }, - { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, - { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, - { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, - { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, - { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, - { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, - { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, - { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, - { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, - { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, - { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, - { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, - { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, - { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, - { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, - { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, - { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, - { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, - { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, - { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, - { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, - { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, - { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, - { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, - { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, - { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, - { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, - { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, - { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, - { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, - { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, - { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, - { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, - { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/92/55fa9ee84cb8ec9930a910b5753936a926f918d8cc8965bdac571479c095/yarl-1.25.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6", size = 144695, upload-time = "2026-09-15T19:29:53.114Z" }, + { url = "https://files.pythonhosted.org/packages/16/b4/9edc8e605b16b2eb5bd0c2ccc73e2b15aab583862460767fb43f91f11839/yarl-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95", size = 104332, upload-time = "2026-09-15T19:29:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/ae/de/4204e6646e278b1cd4a9cf73ebe8b7cfcf16693f1249b324eb273d67602a/yarl-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4", size = 104449, upload-time = "2026-09-15T19:29:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/21d74789198ad68a23a209ab0d73fe5a52d39ebbd9945041e72827acdd77/yarl-1.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059", size = 116828, upload-time = "2026-09-15T19:29:59.031Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/90891ddb61848c067ebd1fb505ab5902a6e7d4707c2f3823f67ff37883d3/yarl-1.25.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c", size = 107150, upload-time = "2026-09-15T19:30:00.868Z" }, + { url = "https://files.pythonhosted.org/packages/a1/15/f3d18743d3688b5f01aa83034b6c00665692ab20e1d0c89a29000d4979ea/yarl-1.25.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126", size = 124704, upload-time = "2026-09-15T19:30:03.128Z" }, + { url = "https://files.pythonhosted.org/packages/99/37/718789d8004775d6a2db86b2afc72b1d156e0590b88a1b8634ddd4bdb8ee/yarl-1.25.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89", size = 129246, upload-time = "2026-09-15T19:30:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/7818ac6dec7fbcd16be2d19495807c01e2c38834e57d41c63356a75e786b/yarl-1.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1", size = 118071, upload-time = "2026-09-15T19:30:06.977Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/8d098cafb30a64df7283b5a5dd0d32d22a71b18a1b9ec071d0776ffe5c2a/yarl-1.25.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb", size = 116180, upload-time = "2026-09-15T19:30:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/fbe9432478d87e00eef80242e15179639df162cb3d5d8d978fc56dcb6c51/yarl-1.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0", size = 116578, upload-time = "2026-09-15T19:30:10.782Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/b3dfd03732236b86b0bcf06a72a37de412ff6f82e719a947c3c651d4dece/yarl-1.25.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326", size = 108886, upload-time = "2026-09-15T19:30:12.951Z" }, + { url = "https://files.pythonhosted.org/packages/83/e1/94dacb650d4963d5f844bc3df7ba70288cb7b68ed5a4d070da71acb19b14/yarl-1.25.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908", size = 124098, upload-time = "2026-09-15T19:30:14.963Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3f/c93f76a218258c7bfc60dd27fd56e9102bdbc625517f4a6ee673295fe4e4/yarl-1.25.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449", size = 115768, upload-time = "2026-09-15T19:30:16.736Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/4d93f13c3b05cda3c962805dec28cbc255c50239b3457808abc5633a00c2/yarl-1.25.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca", size = 122461, upload-time = "2026-09-15T19:30:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e2ba83b3a9bfc1d3b882ad35fa8abc04a7af4d9356f2e329a23d0c8d889d/yarl-1.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426", size = 118295, upload-time = "2026-09-15T19:30:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/42/67/cb5ea1baa0c0ac60bda44ce59f601133154af2a2e67722d3517f8793855d/yarl-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b", size = 102929, upload-time = "2026-09-15T19:30:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/2f/85/9a1e98de10fc0e738d517efd2a9984c3e7db6cf35fa72b2b373988a9e9b7/yarl-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c", size = 98854, upload-time = "2026-09-15T19:30:25.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, ] diff --git a/www/package-lock.json b/www/package-lock.json index 3c425c5e..862f7ab8 100644 --- a/www/package-lock.json +++ b/www/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "lucide-react": "^1.16.0", - "next": "16.2.11", + "next": "16.4.0", "next-themes": "^0.4.6", "react": "^19.2.6", "react-dom": "^19.2.6" @@ -19,7 +19,7 @@ "@types/node": "^25.8.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "postcss": "^8.5.18", + "postcss": "^8.5.23", "tailwindcss": "^4.3.0", "typescript": "^6.0.3" } @@ -47,6 +47,8 @@ }, "node_modules/@img/colour": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, "engines": { @@ -54,9 +56,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -66,19 +68,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -88,19 +90,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -114,9 +135,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -130,9 +151,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -149,9 +170,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -168,9 +189,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -187,9 +208,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -206,9 +227,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -225,9 +246,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -244,9 +265,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -263,9 +284,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -282,9 +303,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -297,19 +318,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -322,19 +343,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -347,19 +368,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -372,19 +393,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -397,19 +418,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -422,19 +443,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -447,19 +468,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -472,38 +493,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -513,16 +550,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -532,14 +569,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -549,7 +588,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -596,16 +635,19 @@ } }, "node_modules/@next/env": { - "version": "16.2.11", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.0.tgz", + "integrity": "sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.11", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.11.tgz", - "integrity": "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.0.tgz", + "integrity": "sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -615,12 +657,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.11", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.11.tgz", - "integrity": "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.0.tgz", + "integrity": "sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -630,12 +673,16 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.11", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.11.tgz", - "integrity": "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.0.tgz", + "integrity": "sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -645,12 +692,16 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.11", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.11.tgz", - "integrity": "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.0.tgz", + "integrity": "sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -660,12 +711,16 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.11", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.11.tgz", - "integrity": "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.0.tgz", + "integrity": "sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -675,12 +730,16 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.11", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.11.tgz", - "integrity": "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.0.tgz", + "integrity": "sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -690,12 +749,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.11", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.11.tgz", - "integrity": "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.0.tgz", + "integrity": "sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -705,7 +765,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.11", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.0.tgz", + "integrity": "sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==", "cpu": [ "x64" ], @@ -719,7 +781,9 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.15", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -1091,7 +1155,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.31", + "version": "2.11.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", + "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -1445,7 +1511,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -1461,14 +1529,16 @@ } }, "node_modules/next": { - "version": "16.2.11", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.0.tgz", + "integrity": "sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==", "license": "MIT", "dependencies": { - "@next/env": "16.2.11", - "@swc/helpers": "0.5.15", + "@next/env": "16.4.0", + "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", + "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "bin": { @@ -1478,15 +1548,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.11", - "@next/swc-darwin-x64": "16.2.11", - "@next/swc-linux-arm64-gnu": "16.2.11", - "@next/swc-linux-arm64-musl": "16.2.11", - "@next/swc-linux-x64-gnu": "16.2.11", - "@next/swc-linux-x64-musl": "16.2.11", - "@next/swc-win32-arm64-msvc": "16.2.11", - "@next/swc-win32-x64-msvc": "16.2.11", - "sharp": "^0.34.5" + "@next/swc-darwin-arm64": "16.4.0", + "@next/swc-darwin-x64": "16.4.0", + "@next/swc-linux-arm64-gnu": "16.4.0", + "@next/swc-linux-arm64-musl": "16.4.0", + "@next/swc-linux-x64-gnu": "16.4.0", + "@next/swc-linux-x64-musl": "16.4.0", + "@next/swc-win32-arm64-msvc": "16.4.0", + "@next/swc-win32-x64-msvc": "16.4.0", + "sharp": "^0.35.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -1519,41 +1589,14 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz", - "integrity": "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==", - "dev": true, + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -1570,7 +1613,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1600,7 +1643,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.1", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "optional": true, "bin": { @@ -1611,50 +1656,59 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "hasInstallScript": true, + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/source-map-js": { - "version": "1.2.1", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -1700,6 +1754,8 @@ }, "node_modules/tslib": { "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/typescript": { diff --git a/www/package.json b/www/package.json index aa13ba17..5d8437ae 100644 --- a/www/package.json +++ b/www/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "lucide-react": "^1.16.0", - "next": "16.2.11", + "next": "16.4.0", "next-themes": "^0.4.6", "react": "^19.2.6", "react-dom": "^19.2.6" @@ -20,7 +20,7 @@ "@types/node": "^25.8.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "postcss": "^8.5.18", + "postcss": "^8.5.23", "tailwindcss": "^4.3.0", "typescript": "^6.0.3" }