diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..4a35f5a --- /dev/null +++ b/.actrc @@ -0,0 +1,5 @@ +--container-architecture linux/amd64 +--artifact-server-path .artifacts +-P ubuntu-latest=catthehacker/ubuntu:act-latest +-P ubuntu-22.04=catthehacker/ubuntu:act-22.04 +-P ubuntu-20.04=catthehacker/ubuntu:act-20.04 diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..7d07811 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,9 @@ +# Example .env.local file - copy to .env.local and customize +# Values here override .env.repo + +# Use a shared tools folder across multiple checkouts +# TOOLS_FOLDER=~/.gpu-tools + +# API tokens (never commit these!) +GITHUB_PAT=your_github_personal_access_token +PYPI_TOKEN=your_pypi_token diff --git a/.env.repo b/.env.repo new file mode 100644 index 0000000..a038f61 --- /dev/null +++ b/.env.repo @@ -0,0 +1,16 @@ +TOOLS_FOLDER=.tools +TOOLS_VERSION_UV=0.9.18 +TOOLS_VERSION_CMAKE=4.2.1 +TOOLS_VERSION_NINJA=1.13.2 +TOOLS_VERSION_EMSDK=4.0.9 # pyodide_2025_0 ABI +TOOLS_VERSION_ACT=0.2.83 +TOOLS_VERSION_GH=2.83.1 +TOOLS_PYTHON313=3.13.11 + +# UV configuration +UV_PYTHON_PREFERENCE=only-managed +UV_CACHE_DIR=${TOOLS_FOLDER}/uv/cache +UV_TOOL_DIR=${TOOLS_FOLDER}/uv/tools +UV_TOOL_BIN_DIR=${TOOLS_FOLDER}/uv/tools/bin +UV_PYTHON_INSTALL_DIR=${TOOLS_FOLDER}/uv/python +UV_PYTHON_BIN_DIR=${TOOLS_FOLDER}/uv/python/bin diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8b68c1a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,97 @@ +name: Tests + +on: + pull_request: + push: + branches: [main] + +jobs: + # Test pure Python packages (gpu-api, gpu-wesl, gpu-canvas) + # Pure Python wheels are version-agnostic (py3-none-any) + test-pure: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + + - name: Restore tools cache + id: cache + uses: actions/cache/restore@v4 + with: + path: ~/.tools + key: tools-${{ runner.os }}-${{ hashFiles('.env.repo', 'activate.sh', 'scripts/**/*.sh') }} + restore-keys: | + tools-${{ runner.os }}- + + - name: Activate environment + env: + TOOLS_FOLDER: ~/.tools + run: source activate.sh + + - name: Save tools cache + if: steps.cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ~/.tools + key: tools-${{ runner.os }}-${{ hashFiles('.env.repo', 'activate.sh', 'scripts/**/*.sh') }} + + - name: Build pure Python packages + run: | + uv build packages/gpu-api -o dist + uv build packages/gpu-wesl -o dist + uv build packages/gpu-canvas -o dist + + - name: Test gpu-api + run: uv run --python ${{ matrix.python-version }} --with dist/gpu_api*.whl --with pytest pytest packages/gpu-api/tests -v + + - name: Test gpu-wesl + run: uv run --python ${{ matrix.python-version }} --with dist/gpu_wesl*.whl --with pytest pytest packages/gpu-wesl/tests -v + + - name: Test gpu-canvas + run: uv run --python ${{ matrix.python-version }} --with dist/gpu_canvas*.whl --with pytest pytest packages/gpu-canvas/tests -v + + # Test native extension packages (gpu-dawn, gpu-wgpu) + # Native wheels are ABI-specific, so we build for each Python version + test-native: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + + - name: Restore tools cache + id: cache + uses: actions/cache/restore@v4 + with: + path: ~/.tools + key: tools-${{ runner.os }}-${{ hashFiles('.env.repo', 'activate.sh', 'scripts/**/*.sh') }} + restore-keys: | + tools-${{ runner.os }}- + + - name: Activate environment + env: + TOOLS_FOLDER: ~/.tools + run: source activate.sh + + - name: Save tools cache + if: steps.cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ~/.tools + key: tools-${{ runner.os }}-${{ hashFiles('.env.repo', 'activate.sh', 'scripts/**/*.sh') }} + + - name: Build and test gpu-dawn + run: | + uv build --python ${{ matrix.python-version }} packages/gpu-dawn -o dist + uv run --python ${{ matrix.python-version }} --with dist/gpu_dawn*.whl --with pytest pytest packages/gpu-dawn/tests -v + + - name: Build and test gpu-wgpu + run: | + rm -rf dist + uv build --python ${{ matrix.python-version }} packages/gpu-wgpu -o dist + uv run --python ${{ matrix.python-version }} --with dist/gpu_wgpu*.whl --with pytest pytest packages/gpu-wgpu/tests -v diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..ebb67f1 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,223 @@ +name: Build wheels + +on: + push: + tags: + - "v*" + pull_request: + workflow_dispatch: + +jobs: + # Build pure Python packages (gpu-api, gpu-wesl, gpu-canvas) + build_pure: + name: Build pure Python packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build gpu-api + run: pipx run build packages/gpu-api + + - name: Build gpu-wesl + run: pipx run build packages/gpu-wesl + + - name: Build gpu-canvas + run: pipx run build packages/gpu-canvas + + - uses: actions/upload-artifact@v4 + with: + name: wheels-pure + path: packages/*/dist/*.whl + + - uses: actions/upload-artifact@v4 + with: + name: sdist-pure + path: packages/*/dist/*.tar.gz + + # Build all native source distributions + build_sdists: + name: Build source distributions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build sdists + run: | + pipx run build --sdist packages/gpu-dawn -o dist/ + pipx run build --sdist packages/gpu-wgpu -o dist/ + pipx run build --sdist packages/gpu-pyodide -o dist/ + + - uses: actions/upload-artifact@v4 + with: + name: sdists + path: dist/*.tar.gz + + # Build gpu-dawn native wheels from sdist + build_dawn: + name: Build gpu-dawn on ${{ matrix.os }} (${{ matrix.arch }}) + needs: [build_sdists] + runs-on: ${{ matrix.runner }} + strategy: + matrix: + include: + - { os: linux, arch: x64, runner: ubuntu-latest, cibw_archs: x86_64 } + - { os: linux, arch: arm64, runner: ubuntu-24.04-arm, cibw_archs: aarch64 } + - { os: macos, arch: x64, runner: macos-15-intel, cibw_archs: x86_64 } + - { os: macos, arch: arm64, runner: macos-latest, cibw_archs: arm64 } + - { os: windows, arch: x64, runner: windows-latest, cibw_archs: AMD64 } + - { os: windows, arch: arm64, runner: windows-11-arm, cibw_archs: ARM64 } + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: sdists + path: sdists/ + + - name: Extract sdist + run: | + mkdir -p package + tar -xzf sdists/gpu_dawn-*.tar.gz -C package --strip-components=1 + shell: bash + + - name: Build wheels + uses: pypa/cibuildwheel@v3.3.1 + with: + package-dir: package + output-dir: wheelhouse + env: + CIBW_ARCHS: ${{ matrix.cibw_archs }} + + - uses: actions/upload-artifact@v4 + with: + name: wheels-dawn-${{ matrix.os }}-${{ matrix.arch }} + path: ./wheelhouse/*.whl + + # Build gpu-wgpu native wheels from sdist + build_wgpu: + name: Build gpu-wgpu on ${{ matrix.os }} (${{ matrix.arch }}) + needs: [build_sdists] + runs-on: ${{ matrix.runner }} + strategy: + matrix: + include: + - { os: linux, arch: x64, runner: ubuntu-latest, cibw_archs: x86_64 } + - { os: linux, arch: arm64, runner: ubuntu-24.04-arm, cibw_archs: aarch64 } + - { os: macos, arch: x64, runner: macos-15-intel, cibw_archs: x86_64 } + - { os: macos, arch: arm64, runner: macos-latest, cibw_archs: arm64 } + - { os: windows, arch: x64, runner: windows-latest, cibw_archs: AMD64 } + - { os: windows, arch: arm64, runner: windows-11-arm, cibw_archs: ARM64 } + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: sdists + path: sdists/ + + - name: Extract sdist + run: | + mkdir -p package + tar -xzf sdists/gpu_wgpu-*.tar.gz -C package --strip-components=1 + shell: bash + + - name: Build wheels + uses: pypa/cibuildwheel@v3.3.1 + with: + package-dir: package + output-dir: wheelhouse + env: + CIBW_ARCHS: ${{ matrix.cibw_archs }} + + - uses: actions/upload-artifact@v4 + with: + name: wheels-wgpu-${{ matrix.os }}-${{ matrix.arch }} + path: ./wheelhouse/*.whl + + # Build gpu-pyodide wheel from sdist (Pyodide only) + build_pyodide: + name: Build gpu-pyodide wheel + needs: [build_sdists] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: sdists + path: sdists/ + + - name: Extract sdist + run: | + mkdir -p package + tar -xzf sdists/gpu_pyodide-*.tar.gz -C package --strip-components=1 + + - name: Build Pyodide wheel + uses: pypa/cibuildwheel@v3.3.1 + with: + package-dir: package + output-dir: wheelhouse + env: + CIBW_PLATFORM: pyodide + + - uses: actions/upload-artifact@v4 + with: + name: wheels-pyodide + path: ./wheelhouse/*.whl + + # Publish to PyPI + publish: + name: Publish to PyPI + needs: [build_pure, build_sdists, build_dawn, build_wgpu, build_pyodide] + runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + + - uses: actions/download-artifact@v4 + with: + name: sdists + path: dist + + - uses: actions/download-artifact@v4 + with: + pattern: sdist-* + path: dist + merge-multiple: true + + # Exclude Pyodide wheels - PyPI doesn't support wasm32 yet (see PEP 783) + - name: Remove Pyodide wheels + run: rm -f dist/*pyodide*.whl dist/*wasm32*.whl dist/*emscripten*.whl + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_TOKEN }} + + # Create GitHub Release with all artifacts + release: + name: Create GitHub Release + needs: [build_pure, build_sdists, build_dawn, build_wgpu, build_pyodide] + runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true diff --git a/.gitignore b/.gitignore index b7faf40..6475177 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,15 @@ +# macOS +.DS_Store + +# Local tools (managed by activate scripts) +.tools/ + +# Act artifacts (local CI testing) +.artifacts/ + +# cibuildwheel output +wheelhouse/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] @@ -136,14 +148,19 @@ celerybeat.pid # Environments .env +.env.local .envrc .venv +.venv-pyodide/ env/ venv/ ENV/ env.bak/ venv.bak/ +# Pyodide build artifacts +**/.pyodide_build/ + # Spyder project settings .spyderproject .spyproject diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7241eb3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,74 @@ +{ + // make the editor format the file when saving + "editor.formatOnSave": true, + // format and organise import in python code + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + }, + // format and organise import in jupyther notebooks + "notebook.formatOnSave.enabled": true, + "notebook.codeActionsOnSave": { + "notebook.source.fixAll": "explicit", + "notebook.source.organizeImports": "explicit" + }, + // automatically format when pasting code if avaible + "editor.formatOnPaste": true, + // every file should have a single final newline, and no training whitespace + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + // show a ruler indicating the maximum line length + "editor.rulers": [ + { + "column": 88, + "color": "#8c768c" + }, + ], + "files.exclude": { + "**/.tools": true, + "**/.cache": true, + "**/.tmp": true, + "**/.coverage": true, + "**/.mypy_cache": true, + "**/.pytest_cache": true, + "**/.ruff_cache": true, + "**/.venv*": true, + "**/htmlcov": true, + "**/site": true + }, + "terminal.integrated.profiles.osx": { + "zsh (activated)": { + "path": "zsh", + "args": [ + "-c", + "source activate.sh && exec zsh" + ] + } + }, + "terminal.integrated.profiles.linux": { + "bash (activated)": { + "path": "bash", + "args": [ + "--rcfile", + "activate.sh" + ] + } + }, + "terminal.integrated.profiles.windows": { + "PowerShell (activated)": { + "source": "PowerShell", + "args": [ + "-NoExit", + "-Command", + ". .\\activate.ps1" + ] + } + }, + "terminal.integrated.defaultProfile.osx": "zsh (activated)", + "terminal.integrated.defaultProfile.linux": "bash (activated)", + "terminal.integrated.defaultProfile.windows": "PowerShell (activated)" +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e9989fd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,142 @@ +# Claude Code Instructions + +This file provides context for Claude Code when working in this repository. + +## Project Overview + +GPU access from Python - a unified WebGPU API for Python across all platforms: + +- **Native Python**: Windows, Linux, macOS (Python 3.10-3.14) +- **WebAssembly**: Pyodide, JupyterLite, browser-based Python (Python 3.12-3.13) + +## Package Structure + +Monorepo with uv workspace. All packages are in `packages/`: + +| Package | Description | Type | +|---------|-------------|------| +| `gpu-api` | Abstract WebGPU API (backend-agnostic) | Pure Python | +| `gpu-dawn` | Dawn (Google Chrome) WebGPU backend | Native extension | +| `gpu-wgpu` | wgpu-native (Rust) WebGPU backend | Native extension | +| `gpu-pyodide` | Dawn Emscripten build for Pyodide/browser | WASM only | +| `gpu-canvas` | Cross-platform windowing | Mixed | +| `gpu-wesl` | WESL shader preprocessing | Pure Python | + +## Environment Setup + +**Always source the activate script before running any commands:** + +```bash +# macOS/Linux +source activate.sh + +# Windows PowerShell +. .\activate.ps1 +``` + +The activate script: +1. Installs development tools (uv, cmake, ninja, emscripten, act) into `.tools/` +2. Sets up PATH and environment variables +3. Creates/syncs the Python virtual environment (`.venv/`) + +Note: The venv is NOT automatically activated to allow working with different Python versions. Use `uv run` to execute commands in the venv. + +## Commands + +After `source activate.sh`, use `uv run` to execute Python tools: + +### Linting and Type Checking + +```bash +source activate.sh + +# Run ruff linting +uv run ruff check . + +# Run ruff formatting check +uv run ruff format --check . + +# Fix formatting issues +uv run ruff format . + +# Run basedpyright type checking +uv run basedpyright +``` + +### Testing + +```bash +source activate.sh + +# Run all tests +uv run pytest packages/*/tests -v + +# Run tests for a specific package +uv run pytest packages/gpu-dawn/tests -v + +# Run tests via duty +uv run duty test +``` + +### Building + +```bash +source activate.sh + +# Build a specific package +uv build packages/gpu-api + +# Build WASM/Pyodide wheel (requires emscripten) +uv run duty build_wasm + +# Build native wheels with cibuildwheel +uv run duty wheels +``` + +### CI + +```bash +source activate.sh + +# Run GitHub Actions locally (requires Docker) +uv run duty ci + +# Run without offline mode +uv run duty ci --no-offline +``` + +## Type Checking Configuration + +- Uses `basedpyright` (not standard pyright) +- Configuration in `pyproject.toml` under `[tool.basedpyright]` +- Only checks authored code via explicit `include` paths +- Native extensions use `.pyi` stub files for type hints + +## Key Files + +- `pyproject.toml` - Workspace configuration, tool settings +- `duties.py` - Development tasks (test, build, release) +- `activate.sh` / `activate.ps1` - Environment setup scripts +- `docs/PLATFORMS.md` - Platform support matrix +- `.github/workflows/` - CI/CD configuration + +## Native Extensions + +The `gpu-dawn` and `gpu-wgpu` packages contain native extensions: +- Source in `src/gpu/{package}/_*.pyx` (Cython) or C/C++ +- Type stubs in `src/gpu/{package}/_*.pyi` +- Build with scikit-build-core + +For `gpu-pyodide`: +- Only runs in Pyodide/Emscripten environment (`sys.platform == "emscripten"`) +- Type stubs in `src/gpu/pyodide/_pyodide.pyi` + +## Git Commit Rules + +Never add the following to commit messages: + +- "Co-Authored-By" lines +- "Generated with" lines or badges +- Any AI attribution + +This is a hard rule with no exceptions. diff --git a/PREREQUISITES.md b/PREREQUISITES.md new file mode 100644 index 0000000..0ca359d --- /dev/null +++ b/PREREQUISITES.md @@ -0,0 +1,109 @@ +# Prerequisites + +This document lists the system-level prerequisites that must be installed before using the development environment. The activate scripts will automatically install managed tools (uv, cmake, ninja, emscripten, python, node). + +## All Platforms + +| Tool | Purpose | +|------|---------| +| git | Clone repositories, version control | + +## Optional (for local CI testing) + +| Tool | Purpose | Install | +|--------|---------------------------|-------------------------------------------------------------| +| Docker | Container runtime for act | [docker.com/get-docker](https://docs.docker.com/get-docker/) | + +## Windows + +| Tool | Purpose | Install | +|------|---------|---------| +| MSVC Build Tools | C/C++ compiler | `winget install Microsoft.VisualStudio.2022.BuildTools` | +| Windows SDK | Platform headers/libs | Included with Build Tools | + +### Installation + +```powershell +# Option 1: winget (recommended) +winget install Microsoft.VisualStudio.2022.BuildTools + +# Option 2: Silent install with specific workload +vs_BuildTools.exe --quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended +``` + +### Notes + +- MinGW is NOT supported for Python extensions (C runtime mismatch since Python 3.5) +- Download size: ~2-6GB +- Requires the "Desktop development with C++" workload + +## Linux + +| Tool | Purpose | Install | +|------|---------|---------| +| GCC/G++ | C/C++ compiler | Package manager | +| python3-dev | Python headers | Package manager | + +### Installation by Distribution + +```bash +# Debian/Ubuntu +sudo apt install build-essential python3-dev + +# Fedora/RHEL/CentOS +sudo dnf install gcc gcc-c++ python3-devel + +# Arch Linux (headers included with python) +sudo pacman -S base-devel +``` + +## macOS + +| Tool | Purpose | Install | +|------|---------|---------| +| Xcode Command Line Tools | Apple Clang + macOS SDK | `xcode-select --install` | + +### Installation + +```bash +xcode-select --install +``` + +### Notes + +- Requires user interaction (license agreement dialog) +- Full Xcode is NOT required (only Command Line Tools) +- Python headers are included with Command Line Tools + +## Verification + +After installing prerequisites, verify your setup: + +```bash +# All platforms +git --version + +# Windows (PowerShell) +cl # Should show MSVC version + +# Linux +gcc --version +python3-config --includes + +# macOS +clang --version +``` + +## Managed Tools + +The following tools are automatically installed by the activate scripts: + +| Tool | Version | Purpose | +|------|---------|---------| +| uv | 0.9.18 | Python package manager | +| cmake | 4.2.1 | Build system generator | +| ninja | 1.13.2 | Fast build system | +| emscripten | 4.0.9 | WebAssembly compiler | +| python | 3.13.3 | Python interpreter (from emsdk) | +| node | 22.16.0 | Node.js runtime (from emsdk) | +| act | 0.2.83 | GitHub Actions runner (optional, requires Docker) | diff --git a/README.md b/README.md index db14519..cb5777e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,41 @@ # gpu -GPU access from python + +GPU access from Python. + +## Vision + +The goal of this project is to provide GPU access to as wide as the Python ecosystem as possible: + +- **Native Python** - Windows, Linux, macOS (Python 3.10-3.14) +- **WebAssembly** - Pyodide, JupyterLite, browser-based Python (Python 3.12-3.13) +- **Multiple backends** - WebGPU, and potentially others in the future + +## Status + +This project is in the **planning stage**. The current package reserves the `gpu` namespace on PyPI while the architecture and implementation are being designed. + +There is no functional code yet. + +## Development + +See [docs/build.md](docs/build.md) for detailed build documentation. + +### Quick Start + +```bash +# macOS/Linux +source activate.sh + +# Windows PowerShell +. .\activate.ps1 +``` + +The activate script automatically installs all development tools (uv, cmake, ninja, emscripten) into the `.tools` folder. + +### Prerequisites + +See [PREREQUISITES.md](PREREQUISITES.md) for system-level requirements (compiler toolchains, etc.). + +## License + +MIT diff --git a/activate.ps1 b/activate.ps1 new file mode 100644 index 0000000..c15ad8f --- /dev/null +++ b/activate.ps1 @@ -0,0 +1,78 @@ +# Source this file: . .\activate.ps1 + +$RepoDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Load .env files (.env first, then .env.local which overrides) +. "$RepoDir\scripts\dotenv.ps1" "$RepoDir\.env" "$RepoDir\.env.local" + +# Export REPO_DIR for future use +$env:REPO_DIR = $RepoDir + +# Expand TOOLS_FOLDER to full path and export +# First expand ~ to home directory if present (USERPROFILE on Windows, HOME on Unix) +if ($env:TOOLS_FOLDER.StartsWith('~')) { + $HomeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $env:HOME } + $env:TOOLS_FOLDER = $env:TOOLS_FOLDER -replace '^~', $HomeDir +} +# Then make relative paths absolute +if ([System.IO.Path]::IsPathRooted($env:TOOLS_FOLDER)) { + # Already absolute, keep as-is +} else { + $env:TOOLS_FOLDER = [System.IO.Path]::Combine($RepoDir, $env:TOOLS_FOLDER) +} + +# Load color utilities +. "$RepoDir/scripts/colors.ps1" + +Write-Host "" +Write-Host "Activating development environment at: " -NoNewline +Write-Host $RepoDir -ForegroundColor Cyan +Write-Host "" +Write-Host "Tools:" + +# Setup uv +$uvResult = . "$RepoDir/scripts/tools/uv.ps1" +if ($uvResult.Status -eq "failed") { + Write-Host " Error: Failed to setup uv" -ForegroundColor Red +} + +# Setup cmake +$cmakeResult = . "$RepoDir/scripts/tools/cmake.ps1" +if ($cmakeResult.Status -eq "failed") { + Write-Host " Error: Failed to setup cmake" -ForegroundColor Red +} + +# Setup ninja +$ninjaResult = . "$RepoDir/scripts/tools/ninja.ps1" +if ($ninjaResult.Status -eq "failed") { + Write-Host " Error: Failed to setup ninja" -ForegroundColor Red +} + +# Setup emscripten +$emsdkResult = . "$RepoDir/scripts/tools/emscripten.ps1" +if ($emsdkResult.Status -eq "failed") { + Write-Host " Error: Failed to setup emscripten" -ForegroundColor Red +} + +# Setup act (optional - requires Docker) +$actResult = . "$RepoDir/scripts/tools/act.ps1" +if ($actResult.Status -eq "failed") { + Write-Host " Error: Failed to setup act" -ForegroundColor Red +} + +# Setup gh (GitHub CLI) +$ghResult = . "$RepoDir/scripts/tools/gh.ps1" +if ($ghResult.Status -eq "failed") { + Write-Host " Error: Failed to setup gh" -ForegroundColor Red +} + +Write-Host "" + +# Sync Python dependencies +uv sync + +Write-Host "Environment activated." + +# Cleanup internal variables and functions +Remove-Variable RepoDir, uvResult, cmakeResult, ninjaResult, emsdkResult, actResult, ghResult, HomeDir -ErrorAction SilentlyContinue +Remove-Item Function:\Write-Tool diff --git a/activate.sh b/activate.sh new file mode 100755 index 0000000..9680126 --- /dev/null +++ b/activate.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Source this file: source activate.sh + +# Get repository root directory (works when sourced) +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Load .env files (.env.repo first, then .env.local which overrides) +source "$REPO_DIR/scripts/dotenv.sh" "$REPO_DIR/.env.repo" "$REPO_DIR/.env.local" + +# Load CI-aware environment helpers (for GITHUB_PATH/GITHUB_ENV support) +source "$REPO_DIR/scripts/cienv.sh" + +# Export REPO_DIR for future use +_export_env REPO_DIR "$REPO_DIR" + +# Expand TOOLS_FOLDER to full path and export +# First expand ~ to $HOME if present +if [[ "$TOOLS_FOLDER" == "~"* ]]; then + TOOLS_FOLDER="$HOME${TOOLS_FOLDER:1}" +fi +# Then make relative paths absolute +if [[ "$TOOLS_FOLDER" = /* ]]; then + _export_env TOOLS_FOLDER "$TOOLS_FOLDER" +else + _export_env TOOLS_FOLDER "$REPO_DIR/$TOOLS_FOLDER" +fi + +# Load color utilities +source "$REPO_DIR/scripts/colors.sh" + +echo "" +printf "${_bold}Activating development environment at:${_reset} ${_cyan}%s${_reset}\n" "$REPO_DIR" +echo "" +echo "Tools:" + +# Setup uv +source "$REPO_DIR/scripts/tools/uv.sh" +if [[ "$SETUP_STATUS" == "failed" ]]; then + echo " Error: Failed to setup uv" >&2 +fi + +# Setup cmake +source "$REPO_DIR/scripts/tools/cmake.sh" +if [[ "$SETUP_STATUS" == "failed" ]]; then + echo " Error: Failed to setup cmake" >&2 +fi + +# Setup ninja +source "$REPO_DIR/scripts/tools/ninja.sh" +if [[ "$SETUP_STATUS" == "failed" ]]; then + echo " Error: Failed to setup ninja" >&2 +fi + +# Setup emscripten +source "$REPO_DIR/scripts/tools/emscripten.sh" +if [[ "$SETUP_STATUS" == "failed" ]]; then + echo " Error: Failed to setup emscripten" >&2 +fi + +# Setup act (optional - requires Docker) +source "$REPO_DIR/scripts/tools/act.sh" +if [[ "$SETUP_STATUS" == "failed" ]]; then + echo " Error: Failed to setup act" >&2 +fi + +# Setup gh (GitHub CLI) +source "$REPO_DIR/scripts/tools/gh.sh" +if [[ "$SETUP_STATUS" == "failed" ]]; then + echo " Error: Failed to setup gh" >&2 +fi + +echo "" + +# Sync Python dependencies +uv sync + +echo "Environment activated." + +# Cleanup internal variables and functions +unset SETUP_STATUS _bold _green _yellow _cyan _reset +unset -f _print_tool _export_path _export_env diff --git a/docs/PLATFORMS.md b/docs/PLATFORMS.md new file mode 100644 index 0000000..4d61b3a --- /dev/null +++ b/docs/PLATFORMS.md @@ -0,0 +1,90 @@ +# Platform Support + +This document describes the platform coverage for the GPU project packages. + +## Overview + +The GPU project provides a unified WebGPU API for Python across all platforms where Python runs - native desktop, servers, and web browsers via Pyodide. + +**End users choose the backend:** + +- **Native Desktop/Server**: Dawn (Google) or wgpu-native (Rust) +- **Browser/Pyodide**: Dawn Emscripten build via gpu-pyodide + +## Platform Tiers + +| Tier | Platforms | Priority | +|------------|-------------------------------------------------------------------|-------------| +| **Tier 1** | Linux x64/ARM64, Windows x64/ARM64, macOS x64/ARM64, Pyodide/WASM | Must have | +| **Tier 2** | Android, iOS, 32-bit (Linux i686, Windows x86) | If interest | + +## Package x Platform Matrix + +| Package | Linux x64 | Linux ARM64 | Win x64 | Win ARM64 | macOS x64 | macOS ARM64 | Pyodide | +|---------|-----------|-------------|---------|-----------|-----------|-------------|---------| +| gpu-api | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| gpu-dawn | Yes | Yes | Yes | Yes | Yes | Yes | No | +| gpu-wgpu | Yes | Yes | Yes | Yes | Yes | Yes | No | +| gpu-pyodide | No | No | No | No | No | No | Yes | +| gpu-canvas | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| gpu-wesl | Yes | Yes | Yes | Yes | Yes | Yes | Yes | + +## Binary Sources + +### Dawn (Google's WebGPU) + +**Primary: [eliemichel/dawn-prebuilt](https://github.com/eliemichel/dawn-prebuilt/releases)** + +- Platforms: Windows/Linux/macOS x64, macOS ARM64, Emscripten +- Versioning: Chromium revisions (e.g., chromium/7187) +- Format: Shared libraries (.dll, .so, .dylib) + Emscripten packages + +**Secondary: [mmozeiko/build-dawn](https://github.com/mmozeiko/build-dawn/releases)** (Windows ARM64) + +- Weekly automated builds +- Windows ARM64 support + +### wgpu-native (Rust WebGPU) + +**Official: [gfx-rs/wgpu-native](https://github.com/gfx-rs/wgpu-native/releases)** + +- Platforms: All Tier 1 native + Windows ARM64 +- Versioning: Semantic (e.g., v27.0.2.0) +- Format: Static + shared libraries with headers + +## Wheels Built + +### Pure Python Packages + +- `gpu-api`: `py3-none-any` +- `gpu-wesl`: `py3-none-any` + +### Native Extension Packages + +**gpu-dawn and gpu-wgpu:** + +- `cp310-cp314 x manylinux_2_28_x86_64` +- `cp310-cp314 x manylinux_2_28_aarch64` +- `cp310-cp314 x win_amd64` +- `cp311-cp314 x win_arm64` (Python 3.11+ only) +- `cp310-cp314 x macosx_10_14_x86_64` +- `cp310-cp314 x macosx_11_0_arm64` + +**gpu-pyodide:** + +- `cp312-cp313 x emscripten_wasm32` + +**gpu-canvas:** + +- TBD (depends on windowing approach) + +## Testing Matrix + +| Package | Native Linux | Native Windows | Native macOS | Pyodide | +|---------|--------------|----------------|--------------|---------| +| gpu-api | Yes | Yes | Yes | Yes | +| gpu-dawn | Yes | Yes | Yes | No | +| gpu-wgpu | Yes | Yes | Yes | No | +| gpu-pyodide | No | No | No | Yes | +| gpu-canvas | Yes | Yes | Yes | Yes | +| gpu-wesl | Yes | Yes | Yes | Yes | diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 0000000..251efc0 --- /dev/null +++ b/docs/build.md @@ -0,0 +1,122 @@ +# Build Documentation + +This document covers the development environment setup and build process. + +## Quick Start + +```bash +# macOS/Linux +source activate.sh + +# Windows PowerShell +. .\activate.ps1 +``` + +The activate script will automatically install all managed tools (uv, cmake, ninja, emscripten, python, node) into the `.tools` folder. + +## Shared Tools Folder + +By default, tools are installed into `.tools/` within the repository. For multiple checkouts or projects, you can share a common tools folder to save disk space and download time. + +Create a `.env.local` file (not committed to git, already in .gitignore): + +```bash +# Share tools across multiple checkouts +TOOLS_FOLDER=~/.shared-tools +``` + +With this configuration: +- All checkouts share the same tools installation +- Saves significant disk space (emscripten alone is ~1GB) +- Reduces initial setup time on new checkouts + +## Automatic Version Management + +Tool versions are tracked in `.env`: + +```bash +TOOLS_VERSION_UV=0.9.18 +TOOLS_VERSION_CMAKE=4.2.1 +TOOLS_VERSION_NINJA=1.13.2 +TOOLS_VERSION_EMSDK=4.0.9 +``` + +When checking out different commits: +- Each commit has its own `.env` with the versions used at that time +- Running `source activate.sh` installs those specific versions +- Multiple versions coexist: `.tools/cmake@4.2.1/`, `.tools/cmake@3.28.0/`, etc. +- Enables reproducible builds across git history + +## CI Caching + +The tools folder structure is designed for efficient CI caching: + +### GitHub Actions + +```yaml +- name: Cache tools + uses: actions/cache@v4 + with: + path: .tools + key: tools-${{ runner.os }} + +- name: Activate environment + run: source activate.sh + shell: bash +``` + +Benefits: + +- Shared cache across workflow runs +- Significant CI time savings (emscripten clone alone takes minutes) +- Version changes are handled automatically (new versions install alongside existing ones) + +## Pyodide Builds + +For WebAssembly/Pyodide builds, the project targets the pyodide_2025_0 ABI: + +| ABI | Python | Emscripten | +|-----|--------|------------| +| pyodide_2025_0 | 3.13 | 4.0.9 | + +The emscripten version in `.env` (4.0.9) matches this ABI. + +## Python Version Support + +| Platform | Python Versions | +|---------------------|------------------------------| +| Windows/Linux/macOS | 3.10, 3.11, 3.12, 3.13, 3.14 | +| Pyodide 2025 | 3.13 | + +The project uses [cibuildwheel](https://cibuildwheel.readthedocs.io/) for cross-platform wheel builds. + +## Directory Structure + +``` +.tools/ # Managed tools (git-ignored) +├── uv@0.9.18/ # uv package manager +├── cmake@4.2.1/ # CMake build system +├── ninja@1.13.2/ # Ninja build system +├── emsdk@4.0.9/ # Emscripten SDK +│ └── upstream/ +│ └── emscripten/ # emcc, em++, etc. +└── uv/ # uv data directories + ├── cache/ + ├── tools/ + └── python/ +``` + +## Troubleshooting + +### Tools not found after activation + +Ensure you're sourcing (not executing) the activate script: + +```bash +# Correct +source activate.sh + +# Incorrect (runs in subshell, changes don't persist) +./activate.sh +bash activate.sh +``` diff --git a/duties.py b/duties.py new file mode 100644 index 0000000..6ef73cb --- /dev/null +++ b/duties.py @@ -0,0 +1,151 @@ +"""Development tasks for the gpu project.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import os +import shutil +import time + +from duty import duty + +if TYPE_CHECKING: + from duty import Context + + +@duty +def test(ctx: Context) -> None: + """Run tests against installed packages.""" + ctx.run( + ( + "uv run pytest " + "packages/gpu-api/tests " + "packages/gpu-dawn/tests " + "packages/gpu-wgpu/tests " + "packages/gpu-wesl/tests " + "packages/gpu-canvas/tests " + "-v" + ), + title="Running tests", + ) + + +@duty +def build(ctx: Context) -> None: + """Build wheel for gpu-api.""" + ctx.run("uv build packages/gpu-api", title="Building gpu-api wheel") + + +PYPI_CONFIG = { + "testpypi": { + "url": "https://test.pypi.org/legacy/", + "token_env": "TEST_PYPI_TOKEN", + }, + "pypi": { + "url": "https://upload.pypi.org/legacy/", + "token_env": "PYPI_TOKEN", + }, +} + + +@duty +def release(ctx: Context, repository: str = "testpypi") -> None: + """Release to PyPI (default: test.pypi.org). + + Args: + ctx: Duty context. + repository: PyPI repository (testpypi or pypi). + """ + config = PYPI_CONFIG.get(repository) + if config: + url = config["url"] + token = os.environ.get(config["token_env"]) + if token: + os.environ["UV_PUBLISH_TOKEN"] = token + else: + url = repository + + ctx.run("uv build packages/gpu-api", title="Building wheel") + ctx.run( + f"uv publish --publish-url {url}", + title=f"Publishing to {repository}", + ) + + +@duty +def ci(ctx: Context, *, offline: bool = True) -> None: + """Run GitHub Actions locally using act. + + Args: + ctx: Duty context. + offline: Run in offline mode (default: True). Set to False to pull images. + """ + start = time.time() + cmd = "act --action-offline-mode" if offline else "act" + try: + ctx.run(cmd, title="Running GitHub Actions locally", capture=False) + finally: + duration = time.time() - start + minutes, seconds = divmod(int(duration), 60) + print(f"\nTotal duration: {minutes}m {seconds}s") + + +@duty +def build_wasm(ctx: Context) -> None: + """Build gpu-dawn wheel for Pyodide/WebAssembly.""" + shutil.rmtree("dist/wasm", ignore_errors=True) + ctx.run( + "uv run pyodide build packages/gpu-dawn --outdir dist/wasm", + title="Building gpu-dawn for Pyodide", + ) + # Workaround: scikit-build-core <0.11.7 on macOS produces wrong platform tag + # (macosx instead of emscripten). Fixed in PR #1196, will be in 0.11.7. + # See: https://github.com/scikit-build/scikit-build-core/issues/920 + # TODO: Remove when scikit-build-core>=0.11.7 is released + ctx.run( + 'uv run python -c "' + + "import glob, subprocess; " + + "wheels = glob.glob('dist/wasm/*.whl'); " + + "[subprocess.run(['uv', 'run', 'wheel', 'tags', '--remove', " + + "'--platform-tag', 'emscripten_4_0_9_wasm32', w]) for w in wheels]" + + '"', + title="Fixing wheel platform tag", + ) + + +@duty +def test_wasm(ctx: Context) -> None: + """Run tests in Pyodide virtual environment.""" + # Clean and recreate pyodide venv + shutil.rmtree(".venv-pyodide", ignore_errors=True) + ctx.run("uv run pyodide venv .venv-pyodide", title="Creating Pyodide venv") + ctx.run( + ".venv-pyodide/bin/pip install dist/wasm/*.whl pytest", + title="Installing wheel in Pyodide venv", + ) + ctx.run( + ".venv-pyodide/bin/python -m pytest packages/gpu-dawn/tests -v", + title="Running tests in Pyodide", + ) + + +@duty +def build_all(ctx: Context) -> None: + """Build both native and Pyodide wheels.""" + ctx.run("uv build packages/gpu-dawn", title="Building native wheel") + build_wasm(ctx) + + +@duty +def wheels(ctx: Context, platform: str = "") -> None: + """Build wheels using cibuildwheel locally. + + Args: + ctx: Duty context. + platform: Target platform (linux, macos, windows, pyodide). Empty for current. + """ + cmd = "uv run cibuildwheel packages/gpu-dawn --output-dir wheelhouse" + if platform: + cmd += f" --platform {platform}" + ctx.run(cmd, title="Building wheels with cibuildwheel") diff --git a/packages/gpu-api/README.md b/packages/gpu-api/README.md new file mode 100644 index 0000000..e077b46 --- /dev/null +++ b/packages/gpu-api/README.md @@ -0,0 +1,17 @@ +# gpu-api + +GPU access from Python. + +## Installation + +```bash +pip install gpu-api +``` + +## Usage + +```python +from gpu.api import hello + +print(hello()) +``` diff --git a/packages/gpu-api/pyproject.toml b/packages/gpu-api/pyproject.toml new file mode 100644 index 0000000..b0e82a9 --- /dev/null +++ b/packages/gpu-api/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "gpu-api" +version = "0.0.0" +description = "GPU access from Python" +readme = "README.md" +license = "MIT" +authors = [{ name = "Willem Kokke", email = "mail@willem.net" }] +requires-python = ">=3.10" +dependencies = [] +classifiers = [ + "Development Status :: 1 - Planning", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries", +] +keywords = ["gpu", "webgpu", "wgpu", "compute", "graphics", "rendering"] + +[project.urls] +Homepage = "https://github.com/willemkokke/gpu" +Repository = "https://github.com/willemkokke/gpu" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/gpu"] diff --git a/packages/gpu-api/src/gpu/api/__init__.py b/packages/gpu-api/src/gpu/api/__init__.py new file mode 100644 index 0000000..4868429 --- /dev/null +++ b/packages/gpu-api/src/gpu/api/__init__.py @@ -0,0 +1,2 @@ +def hello() -> str: + return "Hello from gpu.api!" diff --git a/packages/gpu-api/src/gpu/api/py.typed b/packages/gpu-api/src/gpu/api/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/gpu-api/tests/test_api.py b/packages/gpu-api/tests/test_api.py new file mode 100644 index 0000000..aa6f31d --- /dev/null +++ b/packages/gpu-api/tests/test_api.py @@ -0,0 +1,9 @@ +"""Tests for gpu.api module.""" + +from gpu import api + + +def test_hello_returns_string() -> None: + """Test that hello() returns the expected greeting.""" + result = api.hello() + assert result == "Hello from gpu.api!" diff --git a/packages/gpu-canvas/README.md b/packages/gpu-canvas/README.md new file mode 100644 index 0000000..bb18736 --- /dev/null +++ b/packages/gpu-canvas/README.md @@ -0,0 +1,32 @@ +# gpu-canvas + +Windowing system integration for GPU. + +This package provides cross-platform windowing integration for WebGPU applications: + +- **Native platforms** (Linux, Windows, macOS): Interfaces with OS windowing systems (GLFW, SDL, etc.) +- **Browser/Pyodide**: Interfaces with browser canvas and DOM APIs + +## Installation + +```bash +pip install gpu-canvas +``` + +## Usage + +```python +from gpu.canvas import get_version, get_platform + +version = get_version() # "0.0.0" +platform = get_platform() # "native" or "browser" +``` + +## Platforms + +| Platform | Windowing Backend | +|----------|-------------------| +| Linux | GLFW / SDL | +| Windows | GLFW / SDL | +| macOS | GLFW / SDL | +| Browser/Pyodide | Canvas / DOM | diff --git a/packages/gpu-canvas/pyproject.toml b/packages/gpu-canvas/pyproject.toml new file mode 100644 index 0000000..75cee6e --- /dev/null +++ b/packages/gpu-canvas/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "gpu-canvas" +version = "0.0.0" +description = "Windowing system integration for GPU" +readme = "README.md" +license = "MIT" +authors = [{ name = "Willem Kokke", email = "mail@willem.net" }] +requires-python = ">=3.10" +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/gpu"] diff --git a/packages/gpu-canvas/src/gpu/canvas/__init__.py b/packages/gpu-canvas/src/gpu/canvas/__init__.py new file mode 100644 index 0000000..08377d4 --- /dev/null +++ b/packages/gpu-canvas/src/gpu/canvas/__init__.py @@ -0,0 +1,30 @@ +"""Windowing system integration for GPU. + +This package provides cross-platform windowing integration: +- Native platforms: interfaces with OS windowing (GLFW, SDL, etc.) +- Pyodide/browser: interfaces with browser canvas/DOM +""" + +import sys + +__version__ = "0.0.0" + + +def get_version() -> str: + """Get the package version.""" + return __version__ + + +def get_platform() -> str: + """Get the current platform type. + + Returns: + "browser" if running in Pyodide/browser, + "native" otherwise. + """ + if sys.platform == "emscripten": + return "browser" + return "native" + + +__all__ = ["get_platform", "get_version"] diff --git a/packages/gpu-canvas/src/gpu/canvas/py.typed b/packages/gpu-canvas/src/gpu/canvas/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/gpu-canvas/tests/test_canvas.py b/packages/gpu-canvas/tests/test_canvas.py new file mode 100644 index 0000000..a7fcf2f --- /dev/null +++ b/packages/gpu-canvas/tests/test_canvas.py @@ -0,0 +1,15 @@ +import sys + +from gpu.canvas import get_platform, get_version + + +def test_get_version() -> None: + assert get_version() == "0.0.0" + + +def test_get_platform() -> None: + platform = get_platform() + if sys.platform == "emscripten": + assert platform == "browser" + else: + assert platform == "native" diff --git a/packages/gpu-dawn/CMakeLists.txt b/packages/gpu-dawn/CMakeLists.txt new file mode 100644 index 0000000..232ba2e --- /dev/null +++ b/packages/gpu-dawn/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.15) +project(gpu_dawn LANGUAGES CXX) + +# scikit-build-core provides SKBUILD_PYTHON_PATH with correct Python +find_package(Python 3.10 COMPONENTS Interpreter Development.Module REQUIRED) +find_package(nanobind CONFIG REQUIRED) + +nanobind_add_module(_dawn FREE_THREADED src/_dawn.cpp) + +install(TARGETS _dawn DESTINATION gpu/dawn) + +# Generate stubs for type checking support +# Skip for cross-compilation (Pyodide) - stubs generated separately via inject_stub.py +if(NOT CMAKE_CROSSCOMPILING) + # Delete any existing stub to ensure fresh generation + file(REMOVE ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/dawn/_dawn.pyi) + nanobind_add_stub( + _dawn_stub + MODULE _dawn + OUTPUT _dawn.pyi + PYTHON_PATH $ + DEPENDS _dawn + ) + # Install stub alongside the compiled module + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/_dawn.pyi DESTINATION gpu/dawn) + # Also copy stub to source directory for IDE support during development + add_custom_command( + TARGET _dawn_stub POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/_dawn.pyi + ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/dawn/_dawn.pyi + COMMENT "Copying stub to source directory for IDE support" + ) +endif() diff --git a/packages/gpu-dawn/README.md b/packages/gpu-dawn/README.md new file mode 100644 index 0000000..c3e9682 --- /dev/null +++ b/packages/gpu-dawn/README.md @@ -0,0 +1,18 @@ +# gpu-dawn + +Dawn WebGPU bindings for Python. + +## Installation + +```bash +pip install gpu-dawn +``` + +## Usage + +```python +from gpu.dawn import add, get_backend_name + +result = add(2, 3) # 5 +backend = get_backend_name() # "dawn-dummy" +``` diff --git a/packages/gpu-dawn/pyproject.toml b/packages/gpu-dawn/pyproject.toml new file mode 100644 index 0000000..2cd726e --- /dev/null +++ b/packages/gpu-dawn/pyproject.toml @@ -0,0 +1,58 @@ +[project] +name = "gpu-dawn" +version = "0.0.0" +description = "Dawn WebGPU bindings for Python" +readme = "README.md" +license = "MIT" +authors = [{ name = "Willem Kokke", email = "mail@willem.net" }] +requires-python = ">=3.10" +dependencies = [] + +[build-system] +requires = ["scikit-build-core>=0.10", "nanobind>=2.0"] +build-backend = "scikit_build_core.build" + +[tool.scikit-build] +wheel.packages = ["src/gpu"] +cmake.build-type = "Release" + +[tool.scikit-build.cmake.define] +BUILD_SHARED_LIBS = "OFF" + +[tool.cibuildwheel] +# Build for Python 3.10-3.14 on native platforms, including free-threaded (3.13t, 3.14t) +build = "cp310-* cp311-* cp312-* cp313-* cp313t-* cp314-* cp314t-*" + +# Skip 32-bit builds, musllinux, and Python 3.10 on Windows ARM64 (not available) +skip = "*-win32 *-manylinux_i686 *-musllinux_* cp310-win_arm64" + +# Enable free-threaded builds (3.13t, 3.14t) +enable = ["cpython-freethreading"] + +# Test the built wheels +test-requires = "pytest" +test-command = "pytest {package}/tests -v" + +[tool.cibuildwheel.linux] +archs = ["x86_64", "aarch64"] +manylinux-x86_64-image = "manylinux_2_28" +manylinux-aarch64-image = "manylinux_2_28" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + +[tool.cibuildwheel.macos] +archs = ["x86_64", "arm64"] +repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" + +# macOS deployment targets per architecture +[[tool.cibuildwheel.overrides]] +select = "*-macosx_x86_64" +environment = { MACOSX_DEPLOYMENT_TARGET = "10.15" } + +[[tool.cibuildwheel.overrides]] +select = "*-macosx_arm64" +environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" } + +[tool.cibuildwheel.windows] +archs = ["AMD64", "ARM64"] +before-build = "pip install delvewheel" +repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}" diff --git a/packages/gpu-dawn/scripts/inject_stub.py b/packages/gpu-dawn/scripts/inject_stub.py new file mode 100755 index 0000000..9ec5661 --- /dev/null +++ b/packages/gpu-dawn/scripts/inject_stub.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Inject nanobind-generated stub into a Pyodide wheel. + +This script: +1. Checks if stub already exists in source directory (from cp313 build) +2. If not, creates a Pyodide venv and generates the stub +3. Injects the stub into the wheel + +Usage: + python scripts/inject_stub.py path/to/wheel.whl +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + + +def find_pyodide() -> str: + """Find pyodide executable.""" + pyodide = shutil.which("pyodide") + if pyodide: + return pyodide + raise FileNotFoundError("pyodide executable not found in PATH") + + +def main() -> int: + """Inject stub into wheel.""" + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + return 1 + + wheel_path = Path(sys.argv[1]).resolve() + if not wheel_path.exists(): + print(f"Error: Wheel not found: {wheel_path}", file=sys.stderr) + return 1 + + print(f"Injecting stub into: {wheel_path}") + + # Check if stub already exists in source directory (from cp313 build or native) + script_dir = Path(__file__).resolve().parent + source_stub = script_dir.parent / "src" / "gpu" / "dawn" / "_dawn.pyi" + + if source_stub.exists() and source_stub.stat().st_size > 0: + stub_size = source_stub.stat().st_size + print(f"Using existing stub from {source_stub} ({stub_size} bytes)") + stub_content = source_stub.read_text() + else: + # Generate stub inside Pyodide + print("No existing stub found, generating...") + pyodide = find_pyodide() + print(f"Using pyodide: {pyodide}") + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + venv_path = tmpdir / ".venv-pyodide" + stub_path = tmpdir / "_dawn.pyi" + + # Create Pyodide venv + print("Creating Pyodide venv...") + subprocess.run( + [pyodide, "venv", str(venv_path)], + check=True, + capture_output=True, + ) + + # Install wheel + nanobind + print("Installing wheel and nanobind...") + python = venv_path / "bin" / "python" + result = subprocess.run( + [str(python), "-m", "pip", "install", str(wheel_path), "nanobind"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"pip install failed: {result.stderr}", file=sys.stderr) + return 1 + + # Generate stub using nanobind CLI + print("Generating stub...") + result = subprocess.run( + [ + str(python), + "-m", + "nanobind.stubgen", + "-m", + "gpu.dawn._dawn", + "-o", + str(stub_path), + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"Stubgen failed: {result.stderr}", file=sys.stderr) + return 1 + + if not stub_path.exists() or stub_path.stat().st_size == 0: + print("Error: Stub generation produced empty file", file=sys.stderr) + return 1 + + stub_content = stub_path.read_text() + print(f"Generated stub ({len(stub_content)} bytes)") + + # Copy to source directory for future builds + print(f"Copying stub to {source_stub}") + source_stub.write_text(stub_content) + + # Check if stub already exists in wheel (included from source during build) + stub_in_wheel = "gpu/dawn/_dawn.pyi" + with zipfile.ZipFile(wheel_path, "r") as whl: + if stub_in_wheel in whl.namelist(): + print("Stub already exists in wheel, skipping injection") + print("Done!") + return 0 + + # Inject stub into wheel + print("Injecting stub into wheel...") + with zipfile.ZipFile(wheel_path, "a") as whl: + whl.writestr(stub_in_wheel, stub_content) + + print("Done!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/packages/gpu-dawn/src/_dawn.cpp b/packages/gpu-dawn/src/_dawn.cpp new file mode 100644 index 0000000..e9ffb98 --- /dev/null +++ b/packages/gpu-dawn/src/_dawn.cpp @@ -0,0 +1,17 @@ +#include + +namespace nb = nanobind; + +int add(int a, int b) { + return a + b; +} + +const char* get_backend_name() { + return "dawn-dummy"; +} + +NB_MODULE(_dawn, m) { + m.doc() = "Dawn WebGPU bindings (dummy)"; + m.def("add", &add, "Add two numbers.", nb::arg("a"), nb::arg("b")); + m.def("get_backend_name", &get_backend_name, "Get backend name."); +} diff --git a/packages/gpu-dawn/src/gpu/dawn/__init__.py b/packages/gpu-dawn/src/gpu/dawn/__init__.py new file mode 100644 index 0000000..5ebc912 --- /dev/null +++ b/packages/gpu-dawn/src/gpu/dawn/__init__.py @@ -0,0 +1,5 @@ +"""Dawn WebGPU bindings for Python.""" + +from gpu.dawn._dawn import add, get_backend_name + +__all__ = ["add", "get_backend_name"] diff --git a/packages/gpu-dawn/src/gpu/dawn/_dawn.pyi b/packages/gpu-dawn/src/gpu/dawn/_dawn.pyi new file mode 100644 index 0000000..fcebdd7 --- /dev/null +++ b/packages/gpu-dawn/src/gpu/dawn/_dawn.pyi @@ -0,0 +1,5 @@ +def add(a: int, b: int) -> int: + """Add two numbers.""" + +def get_backend_name() -> str: + """Get backend name.""" diff --git a/packages/gpu-dawn/src/gpu/dawn/py.typed b/packages/gpu-dawn/src/gpu/dawn/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/gpu-dawn/tests/test_dawn.py b/packages/gpu-dawn/tests/test_dawn.py new file mode 100644 index 0000000..fcc4537 --- /dev/null +++ b/packages/gpu-dawn/tests/test_dawn.py @@ -0,0 +1,9 @@ +from gpu.dawn import add, get_backend_name + + +def test_add() -> None: + assert add(2, 3) == 5 + + +def test_backend_name() -> None: + assert get_backend_name() == "dawn-dummy" diff --git a/packages/gpu-pyodide/CMakeLists.txt b/packages/gpu-pyodide/CMakeLists.txt new file mode 100644 index 0000000..9ead52e --- /dev/null +++ b/packages/gpu-pyodide/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.15) +project(gpu_pyodide LANGUAGES CXX) + +# scikit-build-core provides SKBUILD_PYTHON_PATH with correct Python +find_package(Python 3.10 COMPONENTS Interpreter Development.Module REQUIRED) +find_package(nanobind CONFIG REQUIRED) + +nanobind_add_module(_pyodide FREE_THREADED src/_pyodide.cpp) + +install(TARGETS _pyodide DESTINATION gpu/pyodide) + +# Generate stubs for type checking support +# Skip for cross-compilation (Pyodide) - stubs generated separately via inject_stub.py +if(NOT CMAKE_CROSSCOMPILING) + # Delete any existing stub to ensure fresh generation + file(REMOVE ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/pyodide/_pyodide.pyi) + nanobind_add_stub( + _pyodide_stub + MODULE _pyodide + OUTPUT _pyodide.pyi + PYTHON_PATH $ + DEPENDS _pyodide + ) + # Install stub alongside the compiled module + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/_pyodide.pyi DESTINATION gpu/pyodide) + # Also copy stub to source directory for IDE support during development + add_custom_command( + TARGET _pyodide_stub POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/_pyodide.pyi + ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/pyodide/_pyodide.pyi + COMMENT "Copying stub to source directory for IDE support" + ) +endif() diff --git a/packages/gpu-pyodide/README.md b/packages/gpu-pyodide/README.md new file mode 100644 index 0000000..3a0ece0 --- /dev/null +++ b/packages/gpu-pyodide/README.md @@ -0,0 +1,30 @@ +# gpu-pyodide + +Dawn WebGPU bindings for Pyodide/browser environments. + +This package uses the Dawn Emscripten build to provide WebGPU bindings specifically for Pyodide. For native platforms (Linux, Windows, macOS), use `gpu-dawn` or `gpu-wgpu` instead. + +## Installation + +In a Pyodide environment: + +```python +import micropip +await micropip.install("gpu-pyodide") +``` + +## Usage + +```python +from gpu.pyodide import add, get_backend_name + +result = add(2, 3) # 5 +backend = get_backend_name() # "dawn-emscripten-dummy" +``` + +## Note + +This package will raise an `ImportError` if imported on native platforms. Use the appropriate native backend package instead: + +- `gpu-dawn` - Dawn backend for native platforms +- `gpu-wgpu` - wgpu-native backend for native platforms diff --git a/packages/gpu-pyodide/pyproject.toml b/packages/gpu-pyodide/pyproject.toml new file mode 100644 index 0000000..8673bd2 --- /dev/null +++ b/packages/gpu-pyodide/pyproject.toml @@ -0,0 +1,47 @@ +[project] +name = "gpu-pyodide" +version = "0.0.0" +description = "Dawn WebGPU bindings for Pyodide/browser" +readme = "README.md" +license = "MIT" +authors = [{ name = "Willem Kokke", email = "mail@willem.net" }] +requires-python = ">=3.10" +dependencies = [] + +[build-system] +requires = ["scikit-build-core>=0.10", "nanobind>=2.0"] +build-backend = "scikit_build_core.build" + +[tool.scikit-build] +wheel.packages = ["src/gpu"] +cmake.build-type = "Release" + +[tool.scikit-build.cmake.define] +BUILD_SHARED_LIBS = "OFF" + +[tool.cibuildwheel] +# Pyodide only - build cp312 and cp313 for pyodide platform +build = "cp312-pyodide* cp313-pyodide*" + +# Test the built wheels +test-requires = "pytest" + +[tool.cibuildwheel.pyodide] +# Use python -m pytest instead of pytest - script shims are broken in Pyodide venvs +# See: https://github.com/pyodide/pyodide/issues/4802 +test-command = "python -m pytest {package}/tests -v" + +# Workaround: scikit-build-core <0.11.7 on macOS produces wrong platform tag +# (macosx instead of emscripten). Fix by retagging the wheel after build. +# See: https://github.com/scikit-build/scikit-build-core/issues/920 +# TODO: Remove these overrides when scikit-build-core>=0.11.7 is released + +# cp312 uses Pyodide 0.27.x which bundles emsdk 3.1.58 +[[tool.cibuildwheel.overrides]] +select = "cp312-pyodide*" +repair-wheel-command = "python -m wheel tags --remove --platform-tag emscripten_3_1_58_wasm32 {wheel} && python {package}/scripts/inject_stub.py $(dirname {wheel})/*emscripten*.whl && mv $(dirname {wheel})/*emscripten*.whl {dest_dir}/" + +# cp313 uses Pyodide 0.29.x which bundles emsdk 4.0.9 +[[tool.cibuildwheel.overrides]] +select = "cp313-pyodide*" +repair-wheel-command = "python -m wheel tags --remove --platform-tag emscripten_4_0_9_wasm32 {wheel} && python {package}/scripts/inject_stub.py $(dirname {wheel})/*emscripten*.whl && mv $(dirname {wheel})/*emscripten*.whl {dest_dir}/" diff --git a/packages/gpu-pyodide/scripts/inject_stub.py b/packages/gpu-pyodide/scripts/inject_stub.py new file mode 100644 index 0000000..7f89c5a --- /dev/null +++ b/packages/gpu-pyodide/scripts/inject_stub.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Inject nanobind-generated stub into a Pyodide wheel. + +This script: +1. Checks if stub already exists in source directory (from cp313 build) +2. If not, creates a Pyodide venv and generates the stub +3. Injects the stub into the wheel + +Usage: + python scripts/inject_stub.py path/to/wheel.whl +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + + +def find_pyodide() -> str: + """Find pyodide executable.""" + pyodide = shutil.which("pyodide") + if pyodide: + return pyodide + raise FileNotFoundError("pyodide executable not found in PATH") + + +def main() -> int: + """Inject stub into wheel.""" + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + return 1 + + wheel_path = Path(sys.argv[1]).resolve() + if not wheel_path.exists(): + print(f"Error: Wheel not found: {wheel_path}", file=sys.stderr) + return 1 + + print(f"Injecting stub into: {wheel_path}") + + # Check if stub already exists in source directory (from cp313 build or native) + script_dir = Path(__file__).resolve().parent + source_stub = script_dir.parent / "src" / "gpu" / "pyodide" / "_pyodide.pyi" + + if source_stub.exists() and source_stub.stat().st_size > 0: + stub_size = source_stub.stat().st_size + print(f"Using existing stub from {source_stub} ({stub_size} bytes)") + stub_content = source_stub.read_text() + else: + # Generate stub inside Pyodide + print("No existing stub found, generating...") + pyodide = find_pyodide() + print(f"Using pyodide: {pyodide}") + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + venv_path = tmpdir / ".venv-pyodide" + stub_path = tmpdir / "_pyodide.pyi" + + # Create Pyodide venv + print("Creating Pyodide venv...") + subprocess.run( + [pyodide, "venv", str(venv_path)], + check=True, + capture_output=True, + ) + + # Install wheel + nanobind + print("Installing wheel and nanobind...") + python = venv_path / "bin" / "python" + result = subprocess.run( + [str(python), "-m", "pip", "install", str(wheel_path), "nanobind"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"pip install failed: {result.stderr}", file=sys.stderr) + return 1 + + # Generate stub using nanobind CLI + print("Generating stub...") + result = subprocess.run( + [ + str(python), + "-m", + "nanobind.stubgen", + "-m", + "gpu.pyodide._pyodide", + "-o", + str(stub_path), + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"Stubgen failed: {result.stderr}", file=sys.stderr) + return 1 + + if not stub_path.exists() or stub_path.stat().st_size == 0: + print("Error: Stub generation produced empty file", file=sys.stderr) + return 1 + + stub_content = stub_path.read_text() + print(f"Generated stub ({len(stub_content)} bytes)") + + # Copy to source directory for future builds + print(f"Copying stub to {source_stub}") + source_stub.write_text(stub_content) + + # Check if stub already exists in wheel (included from source during build) + stub_in_wheel = "gpu/pyodide/_pyodide.pyi" + with zipfile.ZipFile(wheel_path, "r") as whl: + if stub_in_wheel in whl.namelist(): + print("Stub already exists in wheel, skipping injection") + print("Done!") + return 0 + + # Inject stub into wheel + print("Injecting stub into wheel...") + with zipfile.ZipFile(wheel_path, "a") as whl: + whl.writestr(stub_in_wheel, stub_content) + + print("Done!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/packages/gpu-pyodide/src/_pyodide.cpp b/packages/gpu-pyodide/src/_pyodide.cpp new file mode 100644 index 0000000..42a024a --- /dev/null +++ b/packages/gpu-pyodide/src/_pyodide.cpp @@ -0,0 +1,17 @@ +#include + +namespace nb = nanobind; + +int add(int a, int b) { + return a + b; +} + +const char* get_backend_name() { + return "dawn-emscripten-dummy"; +} + +NB_MODULE(_pyodide, m) { + m.doc() = "Dawn WebGPU bindings for Pyodide (dummy)"; + m.def("add", &add, "Add two numbers.", nb::arg("a"), nb::arg("b")); + m.def("get_backend_name", &get_backend_name, "Get backend name."); +} diff --git a/packages/gpu-pyodide/src/gpu/pyodide/__init__.py b/packages/gpu-pyodide/src/gpu/pyodide/__init__.py new file mode 100644 index 0000000..a8d08c5 --- /dev/null +++ b/packages/gpu-pyodide/src/gpu/pyodide/__init__.py @@ -0,0 +1,18 @@ +"""Dawn WebGPU bindings for Pyodide/browser. + +This package provides WebGPU bindings using the Dawn Emscripten build, +specifically for use in Pyodide/browser environments. +""" + +import sys + +# Check if running in Pyodide +if sys.platform != "emscripten": + raise ImportError( + "gpu-pyodide is only available in Pyodide/browser environments. " + "For native platforms, use gpu-dawn or gpu-wgpu instead." + ) + +from gpu.pyodide._pyodide import add, get_backend_name + +__all__ = ["add", "get_backend_name"] diff --git a/packages/gpu-pyodide/src/gpu/pyodide/_pyodide.pyi b/packages/gpu-pyodide/src/gpu/pyodide/_pyodide.pyi new file mode 100644 index 0000000..fcebdd7 --- /dev/null +++ b/packages/gpu-pyodide/src/gpu/pyodide/_pyodide.pyi @@ -0,0 +1,5 @@ +def add(a: int, b: int) -> int: + """Add two numbers.""" + +def get_backend_name() -> str: + """Get backend name.""" diff --git a/packages/gpu-pyodide/src/gpu/pyodide/py.typed b/packages/gpu-pyodide/src/gpu/pyodide/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/gpu-pyodide/tests/test_pyodide.py b/packages/gpu-pyodide/tests/test_pyodide.py new file mode 100644 index 0000000..808954c --- /dev/null +++ b/packages/gpu-pyodide/tests/test_pyodide.py @@ -0,0 +1,30 @@ +import sys + +import pytest + +# Skip all tests if not running in Pyodide +pytestmark = pytest.mark.skipif( + sys.platform != "emscripten", + reason="gpu-pyodide only works in Pyodide/browser environments", +) + + +def test_add() -> None: + from gpu.pyodide import add + + assert add(2, 3) == 5 + + +def test_backend_name() -> None: + from gpu.pyodide import get_backend_name + + assert get_backend_name() == "dawn-emscripten-dummy" + + +def test_import_error_on_native() -> None: + """Test that importing on native platforms raises ImportError.""" + if sys.platform == "emscripten": + pytest.skip("This test is for native platforms only") + + with pytest.raises(ImportError, match="only available in Pyodide"): + import gpu.pyodide # noqa: F401 diff --git a/packages/gpu-wesl/README.md b/packages/gpu-wesl/README.md new file mode 100644 index 0000000..0aa9d00 --- /dev/null +++ b/packages/gpu-wesl/README.md @@ -0,0 +1,26 @@ +# gpu-wesl + +WESL extension for WGSL manipulation. + +This package provides tools for working with WESL (WebGPU Extended Shader Language), an extension of WGSL that adds preprocessing and manipulation capabilities. + +## Installation + +```bash +pip install gpu-wesl +``` + +## Usage + +```python +from gpu.wesl import get_version + +version = get_version() # "0.0.0" +``` + +## Features + +- WGSL file parsing and manipulation +- WESL preprocessing extensions +- Shader composition and includes +- Works on all platforms (pure Python) diff --git a/packages/gpu-wesl/pyproject.toml b/packages/gpu-wesl/pyproject.toml new file mode 100644 index 0000000..cbfb44a --- /dev/null +++ b/packages/gpu-wesl/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "gpu-wesl" +version = "0.0.0" +description = "WESL extension for WGSL manipulation" +readme = "README.md" +license = "MIT" +authors = [{ name = "Willem Kokke", email = "mail@willem.net" }] +requires-python = ">=3.10" +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/gpu"] diff --git a/packages/gpu-wesl/src/gpu/wesl/__init__.py b/packages/gpu-wesl/src/gpu/wesl/__init__.py new file mode 100644 index 0000000..d6dd277 --- /dev/null +++ b/packages/gpu-wesl/src/gpu/wesl/__init__.py @@ -0,0 +1,15 @@ +"""WESL extension for WGSL manipulation. + +This package provides tools for working with WESL (WebGPU Extended Shader Language), +an extension of WGSL that adds preprocessing and manipulation capabilities. +""" + +__version__ = "0.0.0" + + +def get_version() -> str: + """Get the package version.""" + return __version__ + + +__all__ = ["get_version"] diff --git a/packages/gpu-wesl/src/gpu/wesl/py.typed b/packages/gpu-wesl/src/gpu/wesl/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/gpu-wesl/tests/test_wesl.py b/packages/gpu-wesl/tests/test_wesl.py new file mode 100644 index 0000000..a0802f9 --- /dev/null +++ b/packages/gpu-wesl/tests/test_wesl.py @@ -0,0 +1,5 @@ +from gpu.wesl import get_version + + +def test_get_version() -> None: + assert get_version() == "0.0.0" diff --git a/packages/gpu-wgpu/CMakeLists.txt b/packages/gpu-wgpu/CMakeLists.txt new file mode 100644 index 0000000..c21c3fe --- /dev/null +++ b/packages/gpu-wgpu/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.15) +project(gpu_wgpu LANGUAGES CXX) + +# scikit-build-core provides SKBUILD_PYTHON_PATH with correct Python +find_package(Python 3.10 COMPONENTS Interpreter Development.Module REQUIRED) +find_package(nanobind CONFIG REQUIRED) + +nanobind_add_module(_wgpu FREE_THREADED src/_wgpu.cpp) + +install(TARGETS _wgpu DESTINATION gpu/wgpu) + +# Generate stubs for type checking support +# Skip for cross-compilation (Pyodide) - stubs generated separately via inject_stub.py +if(NOT CMAKE_CROSSCOMPILING) + # Delete any existing stub to ensure fresh generation + file(REMOVE ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/wgpu/_wgpu.pyi) + nanobind_add_stub( + _wgpu_stub + MODULE _wgpu + OUTPUT _wgpu.pyi + PYTHON_PATH $ + DEPENDS _wgpu + ) + # Install stub alongside the compiled module + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/_wgpu.pyi DESTINATION gpu/wgpu) + # Also copy stub to source directory for IDE support during development + add_custom_command( + TARGET _wgpu_stub POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/_wgpu.pyi + ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/wgpu/_wgpu.pyi + COMMENT "Copying stub to source directory for IDE support" + ) +endif() diff --git a/packages/gpu-wgpu/README.md b/packages/gpu-wgpu/README.md new file mode 100644 index 0000000..b96aedc --- /dev/null +++ b/packages/gpu-wgpu/README.md @@ -0,0 +1,18 @@ +# gpu-wgpu + +wgpu-native WebGPU bindings for Python. + +## Installation + +```bash +pip install gpu-wgpu +``` + +## Usage + +```python +from gpu.wgpu import add, get_backend_name + +result = add(2, 3) # 5 +backend = get_backend_name() # "wgpu-native-dummy" +``` diff --git a/packages/gpu-wgpu/pyproject.toml b/packages/gpu-wgpu/pyproject.toml new file mode 100644 index 0000000..c8b49b4 --- /dev/null +++ b/packages/gpu-wgpu/pyproject.toml @@ -0,0 +1,58 @@ +[project] +name = "gpu-wgpu" +version = "0.0.0" +description = "wgpu-native WebGPU bindings for Python" +readme = "README.md" +license = "MIT" +authors = [{ name = "Willem Kokke", email = "mail@willem.net" }] +requires-python = ">=3.10" +dependencies = [] + +[build-system] +requires = ["scikit-build-core>=0.10", "nanobind>=2.0"] +build-backend = "scikit_build_core.build" + +[tool.scikit-build] +wheel.packages = ["src/gpu"] +cmake.build-type = "Release" + +[tool.scikit-build.cmake.define] +BUILD_SHARED_LIBS = "OFF" + +[tool.cibuildwheel] +# Build for Python 3.10-3.14 on native platforms, including free-threaded (3.13t, 3.14t) +build = "cp310-* cp311-* cp312-* cp313-* cp313t-* cp314-* cp314t-*" + +# Skip 32-bit builds, musllinux, and Python 3.10 on Windows ARM64 (not available) +skip = "*-win32 *-manylinux_i686 *-musllinux_* cp310-win_arm64" + +# Enable free-threaded builds (3.13t, 3.14t) +enable = ["cpython-freethreading"] + +# Test the built wheels +test-requires = "pytest" +test-command = "pytest {package}/tests -v" + +[tool.cibuildwheel.linux] +archs = ["x86_64", "aarch64"] +manylinux-x86_64-image = "manylinux_2_28" +manylinux-aarch64-image = "manylinux_2_28" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + +[tool.cibuildwheel.macos] +archs = ["x86_64", "arm64"] +repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" + +# macOS deployment targets per architecture +[[tool.cibuildwheel.overrides]] +select = "*-macosx_x86_64" +environment = { MACOSX_DEPLOYMENT_TARGET = "10.15" } + +[[tool.cibuildwheel.overrides]] +select = "*-macosx_arm64" +environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" } + +[tool.cibuildwheel.windows] +archs = ["AMD64", "ARM64"] +before-build = "pip install delvewheel" +repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}" diff --git a/packages/gpu-wgpu/src/_wgpu.cpp b/packages/gpu-wgpu/src/_wgpu.cpp new file mode 100644 index 0000000..435a932 --- /dev/null +++ b/packages/gpu-wgpu/src/_wgpu.cpp @@ -0,0 +1,17 @@ +#include + +namespace nb = nanobind; + +int add(int a, int b) { + return a + b; +} + +const char* get_backend_name() { + return "wgpu-native-dummy"; +} + +NB_MODULE(_wgpu, m) { + m.doc() = "wgpu-native WebGPU bindings (dummy)"; + m.def("add", &add, "Add two numbers.", nb::arg("a"), nb::arg("b")); + m.def("get_backend_name", &get_backend_name, "Get backend name."); +} diff --git a/packages/gpu-wgpu/src/gpu/wgpu/__init__.py b/packages/gpu-wgpu/src/gpu/wgpu/__init__.py new file mode 100644 index 0000000..343cb97 --- /dev/null +++ b/packages/gpu-wgpu/src/gpu/wgpu/__init__.py @@ -0,0 +1,5 @@ +"""wgpu-native WebGPU bindings for Python.""" + +from gpu.wgpu._wgpu import add, get_backend_name + +__all__ = ["add", "get_backend_name"] diff --git a/packages/gpu-wgpu/src/gpu/wgpu/_wgpu.pyi b/packages/gpu-wgpu/src/gpu/wgpu/_wgpu.pyi new file mode 100644 index 0000000..fcebdd7 --- /dev/null +++ b/packages/gpu-wgpu/src/gpu/wgpu/_wgpu.pyi @@ -0,0 +1,5 @@ +def add(a: int, b: int) -> int: + """Add two numbers.""" + +def get_backend_name() -> str: + """Get backend name.""" diff --git a/packages/gpu-wgpu/src/gpu/wgpu/py.typed b/packages/gpu-wgpu/src/gpu/wgpu/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/gpu-wgpu/tests/test_wgpu.py b/packages/gpu-wgpu/tests/test_wgpu.py new file mode 100644 index 0000000..df63c6b --- /dev/null +++ b/packages/gpu-wgpu/tests/test_wgpu.py @@ -0,0 +1,9 @@ +from gpu.wgpu import add, get_backend_name + + +def test_add() -> None: + assert add(2, 3) == 5 + + +def test_backend_name() -> None: + assert get_backend_name() == "wgpu-native-dummy" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..11bc5c1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,154 @@ +[project] +name = "gpu-workspace" +version = "0.0.0" +description = "GPU access from Python - workspace root" +readme = "README.md" +license = "MIT" +requires-python = ">=3.10" +dependencies = ["gpu-api", "gpu-dawn", "gpu-wgpu", "gpu-canvas", "gpu-wesl"] + +[tool.uv] +managed = true + +[tool.uv.workspace] +members = ["packages/*"] + +[tool.uv.sources] +gpu-api = { workspace = true } +gpu-dawn = { workspace = true } +gpu-wgpu = { workspace = true } +gpu-canvas = { workspace = true } +gpu-wesl = { workspace = true } + +[dependency-groups] +dev = [ + "basedpyright>=1.29", + "cibuildwheel>=3.3.1; python_version >= '3.11'", + "duty>=1.0", + "pyodide-build[test]>=0.30.9; python_version >= '3.13'", + "pytest>=8.0", + "ruff>=0.14.10", +] + +[tool.ruff] + +# Enable preview rules +preview = true + +# Black is 88. +line-length = 88 +indent-width = 4 + +# Assume Python 3.11 +target-version = "py311" + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = [ + "E4", + "E5", + "E7", + "E9", + "F", + "FAST", + "N", + "I", + "D", + "PT", + "UP", + "YTT", + "ANN", + "FBT", + "RUF022", +] +ignore = ["F722", "ANN002", "ANN003"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = true + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" + +[tool.ruff.lint.flake8-annotations] +suppress-dummy-args = true + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.per-file-ignores] +# No need for docstrings in test files or __init__.py files. +"test_*.py" = ["D"] +"__init__.py" = ["D"] + +[tool.ruff.lint.isort] +section-order = [ + "future", + "typing", + "standard-library", + "third-party", + "qt", + "first-party", + "local-folder", + "testing", +] + +[tool.ruff.lint.isort.sections] +testing = ["pytest", "unittest", "mock", "pytestqt"] +typing = ["typing", "typing_extensions"] +qt = ["QT", "PySide6", "PyQt6", "shiboken6", "sip"] + + +# Pyright/Pylance settings (used by VSCode) +# Basedpyright also reads these settings +[tool.basedpyright] +typeCheckingMode = "recommended" +reportAny = false # too much 3rd party code that uses Any +reportExplicitAny = false # too useful an escape hatch +reportUnusedCallResult = false # too noisy, and not that useful +reportMissingModuleSource = "none" # native extensions are stubs only +reportUnannotatedClassAttribute = false # too noisy +reportUnknownVariableType = false # too noisy +reportUnknownMemberType = false # too noisy +reportUnknownArgumentType = false # too noisy +reportUnsafeMultipleInheritance = false # too noisy with mixins +reportImplicitStringConcatenation = false # we use this pattern +deprecateTypingAliases = true +pythonPlatform = "All" +pythonVersion = "3.11" +# Only check our authored code +include = [ + "duties.py", + "packages/*/src", + "packages/*/tests", + "packages/*/scripts", +] diff --git a/scripts/cienv.sh b/scripts/cienv.sh new file mode 100644 index 0000000..a44259a --- /dev/null +++ b/scripts/cienv.sh @@ -0,0 +1,32 @@ +# CI-aware environment helpers for bash +# Source this file to get functions that work in both local and CI environments +# +# Functions: +# _export_path - Add directory to PATH (persists in GitHub Actions) +# _export_env - Export environment variable (persists in GitHub Actions) +# +# Detects GitHub Actions via $GITHUB_ACTIONS environment variable (set by both +# GitHub Actions and nektos/act) + +# Add a directory to PATH, persisting in GitHub Actions +_export_path() { + local dir="$1" + export PATH="$dir:$PATH" + + # In GitHub Actions, also write to GITHUB_PATH for persistence across steps + if [[ -n "$GITHUB_ACTIONS" ]] && [[ -n "$GITHUB_PATH" ]]; then + echo "$dir" >> "$GITHUB_PATH" + fi +} + +# Export an environment variable, persisting in GitHub Actions +_export_env() { + local name="$1" + local value="$2" + export "$name=$value" + + # In GitHub Actions, also write to GITHUB_ENV for persistence across steps + if [[ -n "$GITHUB_ACTIONS" ]] && [[ -n "$GITHUB_ENV" ]]; then + echo "$name=$value" >> "$GITHUB_ENV" + fi +} diff --git a/scripts/colors.ps1 b/scripts/colors.ps1 new file mode 100644 index 0000000..c28dbab --- /dev/null +++ b/scripts/colors.ps1 @@ -0,0 +1,22 @@ +# Color utilities for PowerShell +# Usage: . .\scripts\colors.ps1 +# +# Provides Write-Tool function for consistent tool status output. +# PowerShell's Write-Host gracefully degrades when colors aren't supported. + +function Write-Tool { + param($Name, $Version, $Status) + if ($Status -eq "installing") { + Write-Host " " -NoNewline + Write-Host "Installing" -ForegroundColor Yellow -NoNewline + Write-Host ": " -NoNewline + Write-Host ("{0,-10}" -f $Name) -ForegroundColor Cyan -NoNewline + Write-Host " $Version" + } else { + Write-Host " " -NoNewline + Write-Host "Available" -ForegroundColor Green -NoNewline + Write-Host ": " -NoNewline + Write-Host ("{0,-10}" -f $Name) -ForegroundColor Cyan -NoNewline + Write-Host " $Version" + } +} diff --git a/scripts/colors.sh b/scripts/colors.sh new file mode 100644 index 0000000..abd946b --- /dev/null +++ b/scripts/colors.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Color utilities for Bash/Zsh +# Usage: source scripts/colors.sh +# +# Provides color variables and _print_tool function for consistent output. +# Colors are empty when terminal doesn't support them. + +if [[ -t 1 ]] && [[ "${TERM:-dumb}" != "dumb" ]]; then + _bold='\033[1m' + _green='\033[0;32m' + _yellow='\033[0;33m' + _cyan='\033[0;36m' + _reset='\033[0m' +else + _bold='' + _green='' + _yellow='' + _cyan='' + _reset='' +fi + +# Function to print tool status +_print_tool() { + local name="$1" + local ver="$2" + local state="$3" + if [[ "$state" == "installing" ]]; then + printf " ${_yellow}Installing${_reset}: ${_cyan}%-10s${_reset} %s\n" "$name" "$ver" + else + printf " ${_green}Available${_reset}: ${_cyan}%-10s${_reset} %s\n" "$name" "$ver" + fi +} diff --git a/scripts/dotenv.ps1 b/scripts/dotenv.ps1 new file mode 100644 index 0000000..80da7b5 --- /dev/null +++ b/scripts/dotenv.ps1 @@ -0,0 +1,246 @@ +# Dotenv loader for PowerShell +# Usage: . .\scripts\dotenv.ps1 path/to/.env.repo path/to/.env.local +# +# Features: +# - Two-phase loading: collect values, then substitute variables +# - Later files override earlier files +# - Pre-existing environment variables take precedence (with tilde expansion) +# - Supports $VAR and ${VAR} substitution +# - Single quotes prevent substitution: '$VAR' stays literal +# - Backslash escapes: \$VAR stays literal +# - Supports # comments (full line and end of line) +# - Supports single and double quoted values +# - Expands ~ to $HOME (at start of value only) +# - Non-existent variables become empty string + +param( + [Parameter(Mandatory=$true, Position=0, ValueFromRemainingArguments=$true)] + [string[]]$Files +) + +# Resolved values (after tilde expansion, before variable substitution) +$script:_dotenv_resolved = @{} + +# ---------------------------------------------------------------------------- +# Helper functions +# ---------------------------------------------------------------------------- +function _dotenv_parse_value { + param([string]$raw) + + # Check for quoted values + if ($raw -match '^"(.*)"(\s*#.*)?$') { + # Double-quoted value (quotes stripped, allows substitution later) + return $matches[1] + } + elseif ($raw -match "^'(.*)'(\s*#.*)?$") { + # Single-quoted value (literal, no substitution - mark with prefix) + return "__DOTENV_LITERAL__$($matches[1])" + } + else { + # Unquoted: strip inline comments and trailing whitespace + $value = ($raw -split '#')[0].TrimEnd() + return $value + } +} + +# ---------------------------------------------------------------------------- +# Phase 1: Load files +# ---------------------------------------------------------------------------- + +function _dotenv_load_file { + param([string]$file) + + if (-not (Test-Path $file)) { return } + + Get-Content $file | ForEach-Object { + $line = $_ + + # Skip empty lines and full-line comments + if ([string]::IsNullOrWhiteSpace($line) -or $line -match '^\s*#') { return } + + # Parse KEY=VALUE + if ($line -match '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$') { + $key = $matches[1] + $rawValue = $matches[2] + + # Check if key exists in environment first + $envVal = _dotenv_get_env $key + + if ($envVal -ne "") { + # Use expanded env value (already exported by _dotenv_get_env) + $script:_dotenv_resolved[$key] = $envVal + } + else { + # Parse and expand file value + $value = _dotenv_expand_tilde $rawValue + $value = _dotenv_parse_value $value + $script:_dotenv_resolved[$key] = $value + } + } + } +} + +# ---------------------------------------------------------------------------- +# Phase 2: Variable substitution +# ---------------------------------------------------------------------------- +function _dotenv_lookup { + param([string]$varName) + + # Look up from resolved values (already tilde-expanded) + if ($script:_dotenv_resolved.ContainsKey($varName)) { + return $script:_dotenv_resolved[$varName] + } + + # Not found - return empty string + return "" +} + +function _dotenv_substitute { + param([string]$inputStr) + + # Handle null/empty input + if ([string]::IsNullOrEmpty($inputStr)) { + return "" + } + + $result = "" + $i = 0 + $len = $inputStr.Length + + while ($i -lt $len) { + $char = $inputStr[$i] + $nextChar = if ($i + 1 -lt $len) { $inputStr[$i + 1] } else { $null } + + if ($char -eq '\' -and $nextChar -eq '$') { + # Escaped dollar sign - use placeholder to prevent further substitution + $result += "__DOTENV_ESCAPED_DOLLAR__" + $i += 2 + } + elseif ($char -eq '$') { + if ($nextChar -eq '{') { + # ${VAR} syntax - find closing brace + $rest = $inputStr.Substring($i + 2) + if ($rest -match '^([A-Za-z_][A-Za-z0-9_]*)\}(.*)$') { + $varName = $matches[1] + $varValue = _dotenv_lookup $varName + $result += $varValue + $i += 2 + $varName.Length + 1 # Skip ${, name, } + } + else { + # Invalid syntax, keep as-is + $result += '${' + $i += 2 + } + } + else { + # $VAR syntax - read identifier + $rest = $inputStr.Substring($i + 1) + if ($rest -match '^([A-Za-z_][A-Za-z0-9_]*)(.*)$') { + $varName = $matches[1] + $varValue = _dotenv_lookup $varName + $result += $varValue + $i += 1 + $varName.Length # Skip $ and name + } + else { + # Just a lone $, keep it + $result += '$' + $i += 1 + } + } + } + else { + $result += $char + $i += 1 + } + } + + return $result +} + +function _dotenv_expand_tilde { + param([string]$value) + + # Handle null/empty input + if ([string]::IsNullOrEmpty($value)) { + return "" + } + + if ($value.StartsWith('~')) { + $homeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $env:HOME } + return $value -replace '^~', $homeDir + } + return $value +} + +function _dotenv_set_env { + param([string]$key, [string]$value) + + [Environment]::SetEnvironmentVariable($key, $value, 'Process') + + # In GitHub Actions, also write to GITHUB_ENV for persistence across steps + if ($env:GITHUB_ACTIONS -and $env:GITHUB_ENV) { + Add-Content -Path $env:GITHUB_ENV -Value "$key=$value" + } +} + +function _dotenv_get_env { + param([string]$key) + + $envVal = [Environment]::GetEnvironmentVariable($key, 'Process') + if ($null -ne $envVal -and $envVal -ne "") { + # Expand tilde and re-export immediately + $expanded = _dotenv_expand_tilde $envVal + _dotenv_set_env $key $expanded + return $expanded + } + return "" +} + +# ---------------------------------------------------------------------------- +# Main execution +# ---------------------------------------------------------------------------- + +# Phase 1: Load all files (later files override earlier) +foreach ($file in $Files) { + _dotenv_load_file $file +} + +# Phase 2: Substitute variables until stable, then export +foreach ($key in @($script:_dotenv_resolved.Keys)) { + $value = $script:_dotenv_resolved[$key] + + # Handle null/empty values + if ([string]::IsNullOrEmpty($value)) { + $value = "" + } + # Check for literal marker (from single-quoted values) - no substitution + elseif ($value.StartsWith("__DOTENV_LITERAL__")) { + $value = $value.Substring("__DOTENV_LITERAL__".Length) + } + else { + # Loop until no more substitutions + $prev = "" + while ($value -ne $prev) { + $prev = $value + $value = _dotenv_substitute $value + } + # Replace escaped dollar placeholder with actual dollar sign + $value = $value -replace "__DOTENV_ESCAPED_DOLLAR__", '$' + } + + # Update the stored value (for other vars to reference) + $script:_dotenv_resolved[$key] = $value + + # Export + _dotenv_set_env $key $value +} + +# Cleanup +Remove-Variable -Name '_dotenv_resolved' -Scope Script -ErrorAction SilentlyContinue +Remove-Item -Path Function:\_dotenv_expand_tilde -ErrorAction SilentlyContinue +Remove-Item -Path Function:\_dotenv_set_env -ErrorAction SilentlyContinue +Remove-Item -Path Function:\_dotenv_get_env -ErrorAction SilentlyContinue +Remove-Item -Path Function:\_dotenv_parse_value -ErrorAction SilentlyContinue +Remove-Item -Path Function:\_dotenv_load_file -ErrorAction SilentlyContinue +Remove-Item -Path Function:\_dotenv_lookup -ErrorAction SilentlyContinue +Remove-Item -Path Function:\_dotenv_substitute -ErrorAction SilentlyContinue diff --git a/scripts/dotenv.sh b/scripts/dotenv.sh new file mode 100644 index 0000000..b118b66 --- /dev/null +++ b/scripts/dotenv.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env bash +# Dotenv loader for Bash 4+ / Zsh +# Usage: source scripts/dotenv.sh path/to/.env.repo path/to/.env.local +# +# Features: +# - Two-phase loading: collect values, then substitute variables +# - Later files override earlier files +# - Pre-existing environment variables take precedence (with tilde expansion) +# - Supports $VAR and ${VAR} substitution +# - Single quotes prevent substitution: '$VAR' stays literal +# - Backslash escapes: \$VAR stays literal +# - Supports # comments (full line and end of line) +# - Supports single and double quoted values +# - Expands ~ to $HOME (at start of value only) +# - Non-existent variables become empty string +# +# Requirements: bash 4+ or zsh + +# Check shell compatibility +if [[ -n "${BASH_VERSION:-}" ]]; then + if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then + echo "Error: dotenv.sh requires bash 4+ or zsh (found bash $BASH_VERSION)" >&2 + echo "On macOS, install newer bash: brew install bash" >&2 + return 1 2>/dev/null || exit 1 + fi +fi + +# Resolved values (after tilde expansion, before variable substitution) +declare -A _dotenv_resolved + +# ---------------------------------------------------------------------------- +# Helper functions +# ---------------------------------------------------------------------------- + +_dotenv_expand_tilde() { + local value="$1" + if [[ "$value" == "~"* ]]; then + echo "$HOME${value:1}" + else + echo "$value" + fi +} + +_dotenv_set_env() { + local key="$1" + local value="$2" + export "$key=$value" + + # In GitHub Actions, also write to GITHUB_ENV for persistence across steps + if [[ -n "${GITHUB_ACTIONS:-}" ]] && [[ -n "${GITHUB_ENV:-}" ]]; then + echo "$key=$value" >> "$GITHUB_ENV" + fi +} + +_dotenv_get_env() { + local key="$1" + local env_val + env_val="$(printenv "$key" 2>/dev/null)" || true + + if [[ -n "$env_val" ]]; then + # Expand tilde and re-export immediately + local expanded + expanded="$(_dotenv_expand_tilde "$env_val")" + _dotenv_set_env "$key" "$expanded" + echo "$expanded" + return + fi + + echo "" +} + +_dotenv_parse_value() { + local raw="$1" + local value="" + + # Check for quoted values + if [[ "$raw" =~ ^\"(.*)\"[[:space:]]*(#.*)?$ ]]; then + # Double-quoted value (quotes stripped, allows substitution later) + if [[ -n "${BASH_VERSION:-}" ]]; then + value="${BASH_REMATCH[1]}" + else + value="${match[1]}" + fi + elif [[ "$raw" =~ ^\'(.*)\'[[:space:]]*(#.*)?$ ]]; then + # Single-quoted value (literal, no substitution - mark with prefix) + if [[ -n "${BASH_VERSION:-}" ]]; then + value="__DOTENV_LITERAL__${BASH_REMATCH[1]}" + else + value="__DOTENV_LITERAL__${match[1]}" + fi + else + # Unquoted: strip inline comments and trailing whitespace + value="${raw%%#*}" + # Strip trailing whitespace + value="${value%"${value##*[![:space:]]}"}" + fi + + echo "$value" +} + +_dotenv_process_value() { + local key="$1" + local raw_value="$2" + + # Check if key exists in environment first + local env_val + env_val="$(_dotenv_get_env "$key")" + + if [[ -n "$env_val" ]]; then + # Use expanded env value (already exported by _dotenv_get_env) + _dotenv_resolved[$key]="$env_val" + else + # Parse and expand file value + local value + value="$(_dotenv_expand_tilde "$raw_value")" + value="$(_dotenv_parse_value "$value")" + _dotenv_resolved[$key]="$value" + fi +} + +# ---------------------------------------------------------------------------- +# Phase 1: Load files +# ---------------------------------------------------------------------------- + +_dotenv_load_file() { + local file="$1" + local key raw_value # Declare outside loop (zsh quirk) + [[ -f "$file" ]] || return 0 + + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip empty lines and full-line comments + [[ -z "$line" ]] && continue + [[ "$line" =~ ^[[:space:]]*# ]] && continue + + # Parse KEY=VALUE + if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=[[:space:]]*(.*) ]]; then + if [[ -n "${BASH_VERSION:-}" ]]; then + key="${BASH_REMATCH[1]}" + raw_value="${BASH_REMATCH[2]}" + else + key="${match[1]}" + raw_value="${match[2]}" + fi + + _dotenv_process_value "$key" "$raw_value" + fi + done < "$file" +} + +# ---------------------------------------------------------------------------- +# Phase 2: Variable substitution +# ---------------------------------------------------------------------------- + +_dotenv_lookup() { + local var_name="$1" + + # Look up from resolved values (already tilde-expanded) + if [[ -n "${_dotenv_resolved[$var_name]+x}" ]]; then + echo "${_dotenv_resolved[$var_name]}" + return + fi + + # Not found - return empty string + echo "" +} + +_dotenv_substitute() { + local input="$1" + local result="" + local i=0 + local len=${#input} + local char next_char rest var_name var_value # Declare outside loop (zsh quirk) + + while (( i < len )); do + char="${input:$i:1}" + next_char="${input:$((i+1)):1}" + + if [[ "$char" == "\\" && "$next_char" == "\$" ]]; then + # Escaped dollar sign - use placeholder to prevent further substitution + result+="__DOTENV_ESCAPED_DOLLAR__" + (( i += 2 )) + elif [[ "$char" == "\$" ]]; then + if [[ "$next_char" == "{" ]]; then + # ${VAR} syntax - find closing brace + rest="${input:$((i+2))}" + if [[ "$rest" =~ ^([A-Za-z_][A-Za-z0-9_]*)\}(.*) ]]; then + if [[ -n "${BASH_VERSION:-}" ]]; then + var_name="${BASH_REMATCH[1]}" + else + var_name="${match[1]}" + fi + var_value="$(_dotenv_lookup "$var_name")" + result+="$var_value" + (( i += 2 + ${#var_name} + 1 )) # Skip ${, name, } + else + # Invalid syntax, keep as-is + result+="\${" + (( i += 2 )) + fi + else + # $VAR syntax - read identifier + rest="${input:$((i+1))}" + if [[ "$rest" =~ ^([A-Za-z_][A-Za-z0-9_]*)(.*) ]]; then + if [[ -n "${BASH_VERSION:-}" ]]; then + var_name="${BASH_REMATCH[1]}" + else + var_name="${match[1]}" + fi + var_value="$(_dotenv_lookup "$var_name")" + result+="$var_value" + (( i += 1 + ${#var_name} )) # Skip $ and name + else + # Just a lone $, keep it + result+="\$" + (( i += 1 )) + fi + fi + else + result+="$char" + (( i += 1 )) + fi + done + + echo "$result" +} + +# ---------------------------------------------------------------------------- +# Main execution +# ---------------------------------------------------------------------------- + +# Phase 1: Load all files (later files override earlier) +for _dotenv_file in "$@"; do + _dotenv_load_file "$_dotenv_file" +done + +# Phase 2: Substitute variables until stable, then export +# Get array keys (bash vs zsh syntax) +if [[ -n "${BASH_VERSION:-}" ]]; then + eval '_dotenv_keys=("${!_dotenv_resolved[@]}")' +else + _dotenv_keys=("${(@k)_dotenv_resolved}") +fi + +for _dotenv_key in "${_dotenv_keys[@]}"; do + _dotenv_value="${_dotenv_resolved[$_dotenv_key]}" + + # Check for literal marker (from single-quoted values) - no substitution + if [[ "$_dotenv_value" == "__DOTENV_LITERAL__"* ]]; then + _dotenv_value="${_dotenv_value#__DOTENV_LITERAL__}" + else + # Loop until no more substitutions + _dotenv_prev="" + while [[ "$_dotenv_value" != "$_dotenv_prev" ]]; do + _dotenv_prev="$_dotenv_value" + _dotenv_value="$(_dotenv_substitute "$_dotenv_value")" + done + # Replace escaped dollar placeholder with actual dollar sign + _dotenv_value="${_dotenv_value//__DOTENV_ESCAPED_DOLLAR__/\$}" + fi + + # Update the stored value (for other vars to reference) + _dotenv_resolved[$_dotenv_key]="$_dotenv_value" + + # Export + _dotenv_set_env "$_dotenv_key" "$_dotenv_value" +done + +# Cleanup +unset _dotenv_file _dotenv_key _dotenv_value _dotenv_prev _dotenv_keys +unset _dotenv_resolved +unset -f _dotenv_expand_tilde _dotenv_set_env _dotenv_get_env _dotenv_parse_value +unset -f _dotenv_process_value _dotenv_load_file _dotenv_lookup _dotenv_substitute diff --git a/scripts/tests/dotenv-test.ps1 b/scripts/tests/dotenv-test.ps1 new file mode 100644 index 0000000..c9052ac --- /dev/null +++ b/scripts/tests/dotenv-test.ps1 @@ -0,0 +1,249 @@ +# Dotenv test runner for PowerShell +# Usage: pwsh scripts/tests/dotenv-test.ps1 +# +# This test runner avoids spawning child processes to work around Docker/Rosetta +# emulation issues. Instead, it tracks vars set by dotenv and cleans them up +# after each test. + +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$Fixtures = Join-Path $ScriptDir "fixtures" +$Dotenv = Join-Path $ScriptDir ".." "dotenv.ps1" + +$script:pass = 0 +$script:fail = 0 + +# Track environment variables set during tests for cleanup +$script:testVars = @() + +function Assert-Eq { + param($Name, $Expected, $Actual) + # Normalize null to empty string for comparison + $exp = if ($null -eq $Expected) { "" } else { $Expected } + $act = if ($null -eq $Actual) { "" } else { $Actual } + if ($exp -eq $act) { + Write-Host " " -NoNewline + Write-Host "PASS" -ForegroundColor Green -NoNewline + Write-Host " $Name" + $script:pass++ + } else { + Write-Host " " -NoNewline + Write-Host "FAIL" -ForegroundColor Red -NoNewline + Write-Host " $Name" + Write-Host " expected: '$exp'" + Write-Host " actual: '$act'" + $script:fail++ + } +} + +# Clean up environment variables set by a test +function Clear-TestEnv { + param([string[]]$VarNames) + foreach ($name in $VarNames) { + [Environment]::SetEnvironmentVariable($name, $null, 'Process') + } +} + +function Run-Test { + param($TestName, [scriptblock]$TestBlock) + Write-Host "Test: " -NoNewline + Write-Host $TestName -ForegroundColor Cyan + & $TestBlock + Write-Host "" +} + +# ============================================================================ +# Test 1: Basic Parsing +# ============================================================================ +function Test-Basic { + . $Dotenv "$Fixtures/basic.env" + Assert-Eq "SIMPLE" "value" $env:SIMPLE + Assert-Eq "SPACES_AROUND" "value with spaces" $env:SPACES_AROUND + Assert-Eq "EMPTY" "" $env:EMPTY + Clear-TestEnv @("SIMPLE", "SPACES_AROUND", "EMPTY") +} + +# ============================================================================ +# Test 2: Comments +# ============================================================================ +function Test-Comments { + . $Dotenv "$Fixtures/comments.env" + Assert-Eq "KEY (inline comment stripped)" "value" $env:KEY + Assert-Eq "HASH_IN_QUOTES" "value # not a comment" $env:HASH_IN_QUOTES + Assert-Eq "HASH_IN_SINGLE" "value # not a comment" $env:HASH_IN_SINGLE + Clear-TestEnv @("KEY", "HASH_IN_QUOTES", "HASH_IN_SINGLE") +} + +# ============================================================================ +# Test 3: Quoted Values +# ============================================================================ +function Test-Quotes { + . $Dotenv "$Fixtures/quotes.env" + Assert-Eq "DOUBLE" "double quoted" $env:DOUBLE + Assert-Eq "SINGLE" "single quoted" $env:SINGLE + Assert-Eq "UNQUOTED" "unquoted value" $env:UNQUOTED + Assert-Eq "MIXED" "has 'single' inside" $env:MIXED + Assert-Eq "EMPTY_DOUBLE" "" $env:EMPTY_DOUBLE + Assert-Eq "EMPTY_SINGLE" "" $env:EMPTY_SINGLE + Clear-TestEnv @("DOUBLE", "SINGLE", "UNQUOTED", "MIXED", "EMPTY_DOUBLE", "EMPTY_SINGLE") +} + +# ============================================================================ +# Test 4: Variable Substitution - Basic +# ============================================================================ +function Test-SubstitutionBasic { + . $Dotenv "$Fixtures/substitution-basic.env" + Assert-Eq "BASE" "hello" $env:BASE + Assert-Eq "REF_BRACE (`${BASE})" "hello" $env:REF_BRACE + Assert-Eq "REF_DOLLAR (`$BASE)" "hello" $env:REF_DOLLAR + Assert-Eq "CHAINED" "hello-world" $env:CHAINED + Clear-TestEnv @("BASE", "REF_BRACE", "REF_DOLLAR", "CHAINED") +} + +# ============================================================================ +# Test 5: Variable Substitution - Underscores +# ============================================================================ +function Test-SubstitutionUnderscore { + . $Dotenv "$Fixtures/substitution-underscore.env" + Assert-Eq "VAR_ONE" "first" $env:VAR_ONE + Assert-Eq "VAR_TWO" "second" $env:VAR_TWO + Assert-Eq "USCORE_REF_BRACE (`${VAR_ONE})" "first" $env:USCORE_REF_BRACE + Assert-Eq "USCORE_REF_DOLLAR (`$VAR_ONE)" "first" $env:USCORE_REF_DOLLAR + Assert-Eq "COMBINED" "first_second" $env:COMBINED + Clear-TestEnv @("VAR_ONE", "VAR_TWO", "USCORE_REF_BRACE", "USCORE_REF_DOLLAR", "COMBINED") +} + +# ============================================================================ +# Test 6: Single Quotes Prevent Substitution +# ============================================================================ +function Test-SubstitutionEscape { + . $Dotenv "$Fixtures/substitution-escape.env" + Assert-Eq "ESCAPE_BASE" "hello" $env:ESCAPE_BASE + Assert-Eq "LITERAL_SINGLE (single quotes)" "`$ESCAPE_BASE" $env:LITERAL_SINGLE + Assert-Eq "LITERAL_BRACE_SINGLE (single quotes)" "`${ESCAPE_BASE}" $env:LITERAL_BRACE_SINGLE + Assert-Eq "EXPANDED_DOUBLE (double quotes)" "hello" $env:EXPANDED_DOUBLE + Assert-Eq "BACKSLASH (escaped)" "`$ESCAPE_BASE" $env:BACKSLASH + Clear-TestEnv @("ESCAPE_BASE", "LITERAL_SINGLE", "LITERAL_BRACE_SINGLE", "EXPANDED_DOUBLE", "BACKSLASH") +} + +# ============================================================================ +# Test 7: Non-existent Variables +# ============================================================================ +function Test-SubstitutionMissing { + . $Dotenv "$Fixtures/substitution-missing.env" + Assert-Eq "MISSING" "" $env:MISSING + Assert-Eq "MISSING_BRACE" "" $env:MISSING_BRACE + Assert-Eq "PARTIAL" "prefix--suffix" $env:PARTIAL + Clear-TestEnv @("MISSING", "MISSING_BRACE", "PARTIAL") +} + +# ============================================================================ +# Test 8: Tilde Expansion +# ============================================================================ +function Test-Tilde { + $homeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $env:HOME } + . $Dotenv "$Fixtures/tilde.env" + Assert-Eq "HOME_PATH" "$homeDir/folder" $env:HOME_PATH + Assert-Eq "SUBPATH" "$homeDir/.config/app" $env:SUBPATH + Assert-Eq "NOT_TILDE (middle ~)" "not~tilde" $env:NOT_TILDE + Clear-TestEnv @("HOME_PATH", "SUBPATH", "NOT_TILDE") +} + +# ============================================================================ +# Test 9: Override Behavior - Two Files +# ============================================================================ +function Test-Override { + . $Dotenv "$Fixtures/override-base.env" "$Fixtures/override-local.env" + Assert-Eq "SHARED (local wins)" "from-local" $env:SHARED + Assert-Eq "BASE_ONLY" "base-value" $env:BASE_ONLY + Assert-Eq "LOCAL_ONLY" "local-value" $env:LOCAL_ONLY + Assert-Eq "TO_OVERRIDE" "overridden" $env:TO_OVERRIDE + Clear-TestEnv @("SHARED", "BASE_ONLY", "LOCAL_ONLY", "TO_OVERRIDE") +} + +# ============================================================================ +# Test 10: Environment Variables Never Overwritten +# ============================================================================ +function Test-EnvPriority { + $originalPath = $env:PATH + $env:CUSTOM = 'from-env' + . $Dotenv "$Fixtures/env-priority.env" + Assert-Eq "PATH (not overwritten)" $true ($env:PATH -eq $originalPath) + Assert-Eq "CUSTOM (env wins)" "from-env" $env:CUSTOM + Clear-TestEnv @("CUSTOM") +} + +# ============================================================================ +# Test 11: Cross-file Variable References +# ============================================================================ +function Test-CrossRef { + . $Dotenv "$Fixtures/cross-ref-base.env" "$Fixtures/cross-ref-local.env" + Assert-Eq "BASE_VAR" "base-value" $env:BASE_VAR + Assert-Eq "DERIVED (uses BASE_VAR)" "base-value-extended" $env:DERIVED + Clear-TestEnv @("BASE_VAR", "DERIVED") +} + +# ============================================================================ +# Test 12: Recursive/Chained Substitution +# ============================================================================ +function Test-Chained { + . $Dotenv "$Fixtures/chained.env" + Assert-Eq "A" "1" $env:A + Assert-Eq "B (`$A`$A)" "11" $env:B + Assert-Eq "C (`${B}`${B})" "1111" $env:C + Clear-TestEnv @("A", "B", "C") +} + +# ============================================================================ +# Test 13: Env Var Substitution (CI scenario) +# When an env var is set (e.g., TOOLS_FOLDER=~/.tools from CI), other vars +# that reference it should use the env value, not the .env file value. +# ============================================================================ +function Test-EnvSubstitution { + $homeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $env:HOME } + # Simulate CI environment: TOOLS_FOLDER set with tilde + $env:TOOLS_FOLDER = "~/.tools" + + . $Dotenv "$Fixtures/env-substitution.env" + + # TOOLS_FOLDER should be env value (not .tools from file), with tilde expanded + Assert-Eq "TOOLS_FOLDER (from env, tilde expanded)" "$homeDir/.tools" $env:TOOLS_FOLDER + # UV_CACHE_DIR should use the expanded env value + Assert-Eq "UV_CACHE_DIR (uses env TOOLS_FOLDER)" "$homeDir/.tools/uv/cache" $env:UV_CACHE_DIR + Assert-Eq "UV_PYTHON_DIR (uses env TOOLS_FOLDER)" "$homeDir/.tools/uv/python" $env:UV_PYTHON_DIR + Clear-TestEnv @("TOOLS_FOLDER", "UV_CACHE_DIR", "UV_PYTHON_DIR") +} + +# ============================================================================ +# Run all tests +# ============================================================================ +Write-Host "" +Write-Host "Running dotenv.ps1 tests..." +Write-Host "" + +Run-Test "Basic Parsing" { Test-Basic } +Run-Test "Comments" { Test-Comments } +Run-Test "Quoted Values" { Test-Quotes } +Run-Test "Variable Substitution - Basic" { Test-SubstitutionBasic } +Run-Test "Variable Substitution - Underscores" { Test-SubstitutionUnderscore } +Run-Test "Single Quotes Prevent Substitution" { Test-SubstitutionEscape } +Run-Test "Non-existent Variables" { Test-SubstitutionMissing } +Run-Test "Tilde Expansion" { Test-Tilde } +Run-Test "Override Behavior" { Test-Override } +Run-Test "Environment Variables Never Overwritten" { Test-EnvPriority } +Run-Test "Cross-file Variable References" { Test-CrossRef } +Run-Test "Recursive/Chained Substitution" { Test-Chained } +Run-Test "Env Var Substitution (CI scenario)" { Test-EnvSubstitution } + +# Summary +Write-Host "============================================" +Write-Host "Results: " -NoNewline +Write-Host "$script:pass passed" -ForegroundColor Green -NoNewline +Write-Host ", " -NoNewline +Write-Host "$script:fail failed" -ForegroundColor Red +Write-Host "============================================" + +if ($script:fail -gt 0) { + exit 1 +} diff --git a/scripts/tests/dotenv-test.sh b/scripts/tests/dotenv-test.sh new file mode 100755 index 0000000..62048b5 --- /dev/null +++ b/scripts/tests/dotenv-test.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env zsh +# Dotenv test runner for Zsh (or Bash 4+) +# Usage: zsh scripts/tests/dotenv-test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)" +FIXTURES="$SCRIPT_DIR/fixtures" +DOTENV="$SCRIPT_DIR/../dotenv.sh" + +# Colors (only if terminal supports them) +if [[ -t 1 ]] && [[ "${TERM:-dumb}" != "dumb" ]]; then + _green='\033[0;32m' + _red='\033[0;31m' + _cyan='\033[0;36m' + _reset='\033[0m' +else + _green='' + _red='' + _cyan='' + _reset='' +fi + +# Use temp files for counters (subshells can't modify parent variables) +_pass_file=$(mktemp) +_fail_file=$(mktemp) +echo "0" > "$_pass_file" +echo "0" > "$_fail_file" + +assert_eq() { + local name="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + printf " ${_green}PASS${_reset} %s\n" "$name" + echo $(( $(cat "$_pass_file") + 1 )) > "$_pass_file" + else + printf " ${_red}FAIL${_reset} %s\n" "$name" + printf " expected: '%s'\n" "$expected" + printf " actual: '%s'\n" "$actual" + echo $(( $(cat "$_fail_file") + 1 )) > "$_fail_file" + fi +} + +# Run test in subshell to isolate environment +run_test() { + local test_name="$1" + shift + printf "${_cyan}Test: %s${_reset}\n" "$test_name" + "$@" + echo "" +} + +# ============================================================================ +# Test 1: Basic Parsing +# ============================================================================ +test_basic() { + ( + source "$DOTENV" "$FIXTURES/basic.env" + assert_eq "SIMPLE" "value" "$SIMPLE" + assert_eq "SPACES_AROUND" "value with spaces" "$SPACES_AROUND" + assert_eq "EMPTY" "" "$EMPTY" + ) +} + +# ============================================================================ +# Test 2: Comments +# ============================================================================ +test_comments() { + ( + source "$DOTENV" "$FIXTURES/comments.env" + assert_eq "KEY (inline comment stripped)" "value" "$KEY" + assert_eq "HASH_IN_QUOTES" "value # not a comment" "$HASH_IN_QUOTES" + assert_eq "HASH_IN_SINGLE" "value # not a comment" "$HASH_IN_SINGLE" + ) +} + +# ============================================================================ +# Test 3: Quoted Values +# ============================================================================ +test_quotes() { + ( + source "$DOTENV" "$FIXTURES/quotes.env" + assert_eq "DOUBLE" "double quoted" "$DOUBLE" + assert_eq "SINGLE" "single quoted" "$SINGLE" + assert_eq "UNQUOTED" "unquoted value" "$UNQUOTED" + assert_eq "MIXED" "has 'single' inside" "$MIXED" + assert_eq "EMPTY_DOUBLE" "" "$EMPTY_DOUBLE" + assert_eq "EMPTY_SINGLE" "" "$EMPTY_SINGLE" + ) +} + +# ============================================================================ +# Test 4: Variable Substitution - Basic +# ============================================================================ +test_substitution_basic() { + ( + source "$DOTENV" "$FIXTURES/substitution-basic.env" + assert_eq "BASE" "hello" "$BASE" + assert_eq "REF_BRACE (\${BASE})" "hello" "$REF_BRACE" + assert_eq "REF_DOLLAR (\$BASE)" "hello" "$REF_DOLLAR" + assert_eq "CHAINED" "hello-world" "$CHAINED" + ) +} + +# ============================================================================ +# Test 5: Variable Substitution - Underscores +# ============================================================================ +test_substitution_underscore() { + ( + source "$DOTENV" "$FIXTURES/substitution-underscore.env" + assert_eq "VAR_ONE" "first" "$VAR_ONE" + assert_eq "VAR_TWO" "second" "$VAR_TWO" + assert_eq "USCORE_REF_BRACE (\${VAR_ONE})" "first" "$USCORE_REF_BRACE" + assert_eq "USCORE_REF_DOLLAR (\$VAR_ONE)" "first" "$USCORE_REF_DOLLAR" + assert_eq "COMBINED" "first_second" "$COMBINED" + ) +} + +# ============================================================================ +# Test 6: Single Quotes Prevent Substitution +# ============================================================================ +test_substitution_escape() { + ( + source "$DOTENV" "$FIXTURES/substitution-escape.env" + assert_eq "ESCAPE_BASE" "hello" "$ESCAPE_BASE" + assert_eq "LITERAL_SINGLE (single quotes)" "\$ESCAPE_BASE" "$LITERAL_SINGLE" + assert_eq "LITERAL_BRACE_SINGLE (single quotes)" "\${ESCAPE_BASE}" "$LITERAL_BRACE_SINGLE" + assert_eq "EXPANDED_DOUBLE (double quotes)" "hello" "$EXPANDED_DOUBLE" + assert_eq "BACKSLASH (escaped)" "\$ESCAPE_BASE" "$BACKSLASH" + ) +} + +# ============================================================================ +# Test 7: Non-existent Variables +# ============================================================================ +test_substitution_missing() { + ( + source "$DOTENV" "$FIXTURES/substitution-missing.env" + assert_eq "MISSING" "" "$MISSING" + assert_eq "MISSING_BRACE" "" "$MISSING_BRACE" + assert_eq "PARTIAL" "prefix--suffix" "$PARTIAL" + ) +} + +# ============================================================================ +# Test 8: Tilde Expansion +# ============================================================================ +test_tilde() { + ( + source "$DOTENV" "$FIXTURES/tilde.env" + assert_eq "HOME_PATH" "$HOME/folder" "$HOME_PATH" + assert_eq "SUBPATH" "$HOME/.config/app" "$SUBPATH" + assert_eq "NOT_TILDE (middle ~)" "not~tilde" "$NOT_TILDE" + ) +} + +# ============================================================================ +# Test 9: Override Behavior - Two Files +# ============================================================================ +test_override() { + ( + source "$DOTENV" "$FIXTURES/override-base.env" "$FIXTURES/override-local.env" + assert_eq "SHARED (local wins)" "from-local" "$SHARED" + assert_eq "BASE_ONLY" "base-value" "$BASE_ONLY" + assert_eq "LOCAL_ONLY" "local-value" "$LOCAL_ONLY" + assert_eq "TO_OVERRIDE" "overridden" "$TO_OVERRIDE" + ) +} + +# ============================================================================ +# Test 10: Environment Variables Never Overwritten +# ============================================================================ +test_env_priority() { + ( + # Save original PATH + original_path="$PATH" + # Set a custom var before loading + export CUSTOM="from-env" + + source "$DOTENV" "$FIXTURES/env-priority.env" + + assert_eq "PATH (not overwritten)" "$original_path" "$PATH" + assert_eq "CUSTOM (env wins)" "from-env" "$CUSTOM" + ) +} + +# ============================================================================ +# Test 11: Cross-file Variable References +# ============================================================================ +test_cross_ref() { + ( + source "$DOTENV" "$FIXTURES/cross-ref-base.env" "$FIXTURES/cross-ref-local.env" + assert_eq "BASE_VAR" "base-value" "$BASE_VAR" + assert_eq "DERIVED (uses BASE_VAR)" "base-value-extended" "$DERIVED" + ) +} + +# ============================================================================ +# Test 12: Recursive/Chained Substitution +# ============================================================================ +test_chained() { + ( + source "$DOTENV" "$FIXTURES/chained.env" + assert_eq "A" "1" "$A" + assert_eq "B (\$A\$A)" "11" "$B" + assert_eq "C (\${B}\${B})" "1111" "$C" + ) +} + +# ============================================================================ +# Test 13: Env Var Substitution (CI scenario) +# When an env var is set (e.g., TOOLS_FOLDER=~/.tools from CI), other vars +# that reference it should use the env value, not the .env file value. +# ============================================================================ +test_env_substitution() { + ( + # Simulate CI environment: TOOLS_FOLDER set with tilde + export TOOLS_FOLDER="~/.tools" + + source "$DOTENV" "$FIXTURES/env-substitution.env" + + # TOOLS_FOLDER should be env value (not .tools from file), with tilde expanded + assert_eq "TOOLS_FOLDER (from env, tilde expanded)" "$HOME/.tools" "$TOOLS_FOLDER" + # UV_CACHE_DIR should use the expanded env value + assert_eq "UV_CACHE_DIR (uses env TOOLS_FOLDER)" "$HOME/.tools/uv/cache" "$UV_CACHE_DIR" + assert_eq "UV_PYTHON_DIR (uses env TOOLS_FOLDER)" "$HOME/.tools/uv/python" "$UV_PYTHON_DIR" + ) +} + +# ============================================================================ +# Run all tests +# ============================================================================ +echo "" +echo "Running dotenv.sh tests..." +echo "" + +run_test "Basic Parsing" test_basic +run_test "Comments" test_comments +run_test "Quoted Values" test_quotes +run_test "Variable Substitution - Basic" test_substitution_basic +run_test "Variable Substitution - Underscores" test_substitution_underscore +run_test "Single Quotes Prevent Substitution" test_substitution_escape +run_test "Non-existent Variables" test_substitution_missing +run_test "Tilde Expansion" test_tilde +run_test "Override Behavior" test_override +run_test "Environment Variables Never Overwritten" test_env_priority +run_test "Cross-file Variable References" test_cross_ref +run_test "Recursive/Chained Substitution" test_chained +run_test "Env Var Substitution (CI scenario)" test_env_substitution + +# Summary +pass=$(cat "$_pass_file") +fail=$(cat "$_fail_file") +rm -f "$_pass_file" "$_fail_file" + +echo "============================================" +printf "Results: ${_green}%d passed${_reset}, ${_red}%d failed${_reset}\n" "$pass" "$fail" +echo "============================================" + +if [[ $fail -gt 0 ]]; then + exit 1 +fi diff --git a/scripts/tests/fixtures/basic.env b/scripts/tests/fixtures/basic.env new file mode 100644 index 0000000..bad999f --- /dev/null +++ b/scripts/tests/fixtures/basic.env @@ -0,0 +1,4 @@ +# Test: Basic parsing +SIMPLE=value +SPACES_AROUND = value with spaces +EMPTY= diff --git a/scripts/tests/fixtures/chained.env b/scripts/tests/fixtures/chained.env new file mode 100644 index 0000000..c32a3c4 --- /dev/null +++ b/scripts/tests/fixtures/chained.env @@ -0,0 +1,4 @@ +# Test: Recursive/chained substitution +A=1 +B=$A$A +C=${B}${B} diff --git a/scripts/tests/fixtures/comments.env b/scripts/tests/fixtures/comments.env new file mode 100644 index 0000000..3d71082 --- /dev/null +++ b/scripts/tests/fixtures/comments.env @@ -0,0 +1,5 @@ +# Test: Comments handling +# Full line comment +KEY=value # inline comment +HASH_IN_QUOTES="value # not a comment" +HASH_IN_SINGLE='value # not a comment' diff --git a/scripts/tests/fixtures/cross-ref-base.env b/scripts/tests/fixtures/cross-ref-base.env new file mode 100644 index 0000000..b465c59 --- /dev/null +++ b/scripts/tests/fixtures/cross-ref-base.env @@ -0,0 +1,2 @@ +# Test: Cross-file variable references - base file +BASE_VAR=base-value diff --git a/scripts/tests/fixtures/cross-ref-local.env b/scripts/tests/fixtures/cross-ref-local.env new file mode 100644 index 0000000..273203a --- /dev/null +++ b/scripts/tests/fixtures/cross-ref-local.env @@ -0,0 +1,2 @@ +# Test: Cross-file variable references - local file +DERIVED=${BASE_VAR}-extended diff --git a/scripts/tests/fixtures/env-priority.env b/scripts/tests/fixtures/env-priority.env new file mode 100644 index 0000000..10be4f0 --- /dev/null +++ b/scripts/tests/fixtures/env-priority.env @@ -0,0 +1,3 @@ +# Test: Environment variables never overwritten +PATH=/should/not/override +CUSTOM=from-file diff --git a/scripts/tests/fixtures/env-substitution.env b/scripts/tests/fixtures/env-substitution.env new file mode 100644 index 0000000..dd081b8 --- /dev/null +++ b/scripts/tests/fixtures/env-substitution.env @@ -0,0 +1,6 @@ +# Test: Substitution should use expanded env vars, not file values +# Scenario: TOOLS_FOLDER is set in environment (e.g., CI sets ~/.tools) +# but .env has a different default value. Other vars reference TOOLS_FOLDER. +TOOLS_FOLDER=.tools +UV_CACHE_DIR=${TOOLS_FOLDER}/uv/cache +UV_PYTHON_DIR=${TOOLS_FOLDER}/uv/python diff --git a/scripts/tests/fixtures/override-base.env b/scripts/tests/fixtures/override-base.env new file mode 100644 index 0000000..1d44023 --- /dev/null +++ b/scripts/tests/fixtures/override-base.env @@ -0,0 +1,4 @@ +# Test: Override behavior - base file +SHARED=from-base +BASE_ONLY=base-value +TO_OVERRIDE=original diff --git a/scripts/tests/fixtures/override-local.env b/scripts/tests/fixtures/override-local.env new file mode 100644 index 0000000..99c4b4d --- /dev/null +++ b/scripts/tests/fixtures/override-local.env @@ -0,0 +1,4 @@ +# Test: Override behavior - local file (loaded second, overrides base) +SHARED=from-local +LOCAL_ONLY=local-value +TO_OVERRIDE=overridden diff --git a/scripts/tests/fixtures/quotes.env b/scripts/tests/fixtures/quotes.env new file mode 100644 index 0000000..8d7a584 --- /dev/null +++ b/scripts/tests/fixtures/quotes.env @@ -0,0 +1,7 @@ +# Test: Quoted values +DOUBLE="double quoted" +SINGLE='single quoted' +UNQUOTED=unquoted value +MIXED="has 'single' inside" +EMPTY_DOUBLE="" +EMPTY_SINGLE='' diff --git a/scripts/tests/fixtures/substitution-basic.env b/scripts/tests/fixtures/substitution-basic.env new file mode 100644 index 0000000..3dca887 --- /dev/null +++ b/scripts/tests/fixtures/substitution-basic.env @@ -0,0 +1,5 @@ +# Test: Variable substitution - basic +BASE=hello +REF_BRACE=${BASE} +REF_DOLLAR=$BASE +CHAINED=${REF_BRACE}-world diff --git a/scripts/tests/fixtures/substitution-escape.env b/scripts/tests/fixtures/substitution-escape.env new file mode 100644 index 0000000..aaa0291 --- /dev/null +++ b/scripts/tests/fixtures/substitution-escape.env @@ -0,0 +1,6 @@ +# Test: Escaping substitution +ESCAPE_BASE=hello +LITERAL_SINGLE='$ESCAPE_BASE' +LITERAL_BRACE_SINGLE='${ESCAPE_BASE}' +EXPANDED_DOUBLE="$ESCAPE_BASE" +BACKSLASH=\$ESCAPE_BASE diff --git a/scripts/tests/fixtures/substitution-missing.env b/scripts/tests/fixtures/substitution-missing.env new file mode 100644 index 0000000..4c93c21 --- /dev/null +++ b/scripts/tests/fixtures/substitution-missing.env @@ -0,0 +1,4 @@ +# Test: Non-existent variables +MISSING=$DOES_NOT_EXIST +MISSING_BRACE=${DOES_NOT_EXIST} +PARTIAL=prefix-$MISSING_VAR-suffix diff --git a/scripts/tests/fixtures/substitution-underscore.env b/scripts/tests/fixtures/substitution-underscore.env new file mode 100644 index 0000000..16eec6f --- /dev/null +++ b/scripts/tests/fixtures/substitution-underscore.env @@ -0,0 +1,6 @@ +# Test: Variable substitution - underscores in names +VAR_ONE=first +VAR_TWO=second +USCORE_REF_BRACE=${VAR_ONE} +USCORE_REF_DOLLAR=$VAR_ONE +COMBINED=${VAR_ONE}_${VAR_TWO} diff --git a/scripts/tests/fixtures/tilde.env b/scripts/tests/fixtures/tilde.env new file mode 100644 index 0000000..d41f3ca --- /dev/null +++ b/scripts/tests/fixtures/tilde.env @@ -0,0 +1,4 @@ +# Test: Tilde expansion +HOME_PATH=~/folder +SUBPATH=~/.config/app +NOT_TILDE=not~tilde diff --git a/scripts/tools/act.ps1 b/scripts/tools/act.ps1 new file mode 100644 index 0000000..0101633 --- /dev/null +++ b/scripts/tools/act.ps1 @@ -0,0 +1,128 @@ +# Tool script for act (GitHub Actions local runner) +# Dot-source this file after setting $env:TOOLS_FOLDER and $env:TOOLS_VERSION_ACT +# +# Requires (environment variables): +# $env:TOOLS_FOLDER - Base folder for tools (absolute path) +# $env:TOOLS_VERSION_ACT - Version to install (e.g., 0.2.83) +# +# Returns (hashtable): +# @{ Status = "installed" | "available" | "skipped" | "failed" } +# +# Tools made available: +# act +# +# Behavior: +# - Checks if Docker is available (skips if not - act requires Docker) +# - Checks if act is already installed (early exit if yes) +# - Prints status via Write-Tool (if available) +# - Downloads prebuilt binary from GitHub releases +# - Adds act to PATH + +# Validate required variables +if (-not $env:TOOLS_FOLDER -or -not $env:TOOLS_VERSION_ACT) { + Write-Error "TOOLS_FOLDER and TOOLS_VERSION_ACT must be set" + return @{ Status = "failed" } +} + +# Check if Docker is available (act requires Docker) +if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "act" $env:TOOLS_VERSION_ACT "skipped" + } + return @{ Status = "skipped" } +} + +$_actDir = [System.IO.Path]::Combine($env:TOOLS_FOLDER, "act@$env:TOOLS_VERSION_ACT") + +if (-not (Test-Path $_actDir)) { + # Print status if Write-Tool is available + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "act" $env:TOOLS_VERSION_ACT "installing" + } + + # Determine platform and architecture + $_os = if ($IsWindows -or $env:OS -eq "Windows_NT") { "Windows" } elseif ($IsMacOS) { "Darwin" } else { "Linux" } + $_arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() + + switch ($_os) { + "Darwin" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/nektos/act/releases/download/v$env:TOOLS_VERSION_ACT/act_Darwin_x86_64.tar.gz" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/nektos/act/releases/download/v$env:TOOLS_VERSION_ACT/act_Darwin_arm64.tar.gz" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + } + "Linux" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/nektos/act/releases/download/v$env:TOOLS_VERSION_ACT/act_Linux_x86_64.tar.gz" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/nektos/act/releases/download/v$env:TOOLS_VERSION_ACT/act_Linux_arm64.tar.gz" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + } + "Windows" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/nektos/act/releases/download/v$env:TOOLS_VERSION_ACT/act_Windows_x86_64.zip" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/nektos/act/releases/download/v$env:TOOLS_VERSION_ACT/act_Windows_arm64.zip" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + } + } + + # Download and extract + New-Item -ItemType Directory -Path $_actDir -Force | Out-Null + $_tempFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "act-$env:TOOLS_VERSION_ACT$( if ($_os -eq 'Windows') { '.zip' } else { '.tar.gz' } )") + + try { + Invoke-WebRequest -Uri $_url -OutFile $_tempFile -UseBasicParsing + + if ($_os -eq "Windows") { + Expand-Archive -Path $_tempFile -DestinationPath $_actDir -Force + } else { + tar -xzf $_tempFile -C $_actDir + chmod +x "$_actDir/act" 2>$null + } + + $_status = "installed" + } catch { + Remove-Item -Path $_actDir -Recurse -Force -ErrorAction SilentlyContinue + $_status = "failed" + } finally { + Remove-Item -Path $_tempFile -Force -ErrorAction SilentlyContinue + } +} else { + # Already installed + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "act" $env:TOOLS_VERSION_ACT "available" + } + $_status = "available" +} + +# Add to PATH +if ($_status -ne "failed" -and $_status -ne "skipped") { + $PathSep = [System.IO.Path]::PathSeparator + $env:PATH = "$_actDir$PathSep$env:PATH" +} + +# Return result +@{ Status = $_status } diff --git a/scripts/tools/act.sh b/scripts/tools/act.sh new file mode 100644 index 0000000..bdd7c8b --- /dev/null +++ b/scripts/tools/act.sh @@ -0,0 +1,107 @@ +# Tool script for act (GitHub Actions local runner) +# Source this file after setting TOOLS_FOLDER and TOOLS_VERSION_ACT +# +# Requires (environment variables): +# TOOLS_FOLDER - Base folder for tools (absolute path) +# TOOLS_VERSION_ACT - Version to install (e.g., 0.2.83) +# +# Provides (after sourcing): +# SETUP_STATUS - "installed" | "available" | "skipped" | "failed" +# +# Tools made available: +# act +# +# Behavior: +# - Checks if Docker is available (skips if not - act requires Docker) +# - Checks if act is already installed (early exit if yes) +# - Prints status via _print_tool (if available) +# - Downloads prebuilt binary from GitHub releases +# - Adds act to PATH + +# Validate required variables +if [[ -z "$TOOLS_FOLDER" ]] || [[ -z "$TOOLS_VERSION_ACT" ]]; then + echo "Error: TOOLS_FOLDER and TOOLS_VERSION_ACT must be set" >&2 + SETUP_STATUS="failed" + return 1 +fi + +# Check if Docker is available (act requires Docker) +if ! command -v docker &>/dev/null; then + type _print_tool &>/dev/null && _print_tool "act" "$TOOLS_VERSION_ACT" "skipped" + SETUP_STATUS="skipped" + return 0 +fi + +_act_dir="$TOOLS_FOLDER/act@$TOOLS_VERSION_ACT" + +if [[ ! -d "$_act_dir" ]]; then + # Print status if _print_tool is available + type _print_tool &>/dev/null && _print_tool "act" "$TOOLS_VERSION_ACT" "installing" + + # Determine platform and architecture + _os="$(uname -s)" + _arch="$(uname -m)" + + case "$_os" in + Darwin) + case "$_arch" in + x86_64) + _url="https://github.com/nektos/act/releases/download/v${TOOLS_VERSION_ACT}/act_Darwin_x86_64.tar.gz" + ;; + arm64) + _url="https://github.com/nektos/act/releases/download/v${TOOLS_VERSION_ACT}/act_Darwin_arm64.tar.gz" + ;; + *) + echo "Error: Unsupported architecture: $_arch" >&2 + SETUP_STATUS="failed" + unset _act_dir _os _arch + return 1 + ;; + esac + ;; + Linux) + case "$_arch" in + x86_64) + _url="https://github.com/nektos/act/releases/download/v${TOOLS_VERSION_ACT}/act_Linux_x86_64.tar.gz" + ;; + aarch64|arm64) + _url="https://github.com/nektos/act/releases/download/v${TOOLS_VERSION_ACT}/act_Linux_arm64.tar.gz" + ;; + *) + echo "Error: Unsupported architecture: $_arch" >&2 + SETUP_STATUS="failed" + unset _act_dir _os _arch + return 1 + ;; + esac + ;; + *) + echo "Error: Unsupported OS: $_os" >&2 + SETUP_STATUS="failed" + unset _act_dir _os _arch + return 1 + ;; + esac + + # Download and extract + mkdir -p "$_act_dir" + if curl -fsSL "$_url" | tar -xz -C "$_act_dir" 2>/dev/null; then + chmod +x "$_act_dir/act" + SETUP_STATUS="installed" + else + rm -rf "$_act_dir" + SETUP_STATUS="failed" + fi + + unset _url _os _arch +else + type _print_tool &>/dev/null && _print_tool "act" "$TOOLS_VERSION_ACT" "available" + SETUP_STATUS="available" +fi + +# Add to PATH +if [[ "$SETUP_STATUS" != "failed" ]] && [[ "$SETUP_STATUS" != "skipped" ]]; then + _export_path "$_act_dir" +fi + +unset _act_dir diff --git a/scripts/tools/cmake.ps1 b/scripts/tools/cmake.ps1 new file mode 100644 index 0000000..c386712 --- /dev/null +++ b/scripts/tools/cmake.ps1 @@ -0,0 +1,121 @@ +# Tool script for CMake (cross-platform build system) +# Dot-source this file after setting $env:TOOLS_FOLDER and $env:TOOLS_VERSION_CMAKE +# +# Requires (environment variables): +# $env:TOOLS_FOLDER - Base folder for tools (absolute path) +# $env:TOOLS_VERSION_CMAKE - Version to install (e.g., 4.2.1) +# +# Returns (hashtable): +# @{ Status = "installed" | "available" | "failed" } +# +# Tools made available: +# cmake, ctest, cpack +# +# Behavior: +# - Checks if cmake is already installed (early exit if yes) +# - Prints status via Write-Tool (if available) +# - Downloads prebuilt binary from GitHub releases +# - Adds cmake to PATH + +# Validate required variables +if (-not $env:TOOLS_FOLDER -or -not $env:TOOLS_VERSION_CMAKE) { + Write-Error "TOOLS_FOLDER and TOOLS_VERSION_CMAKE must be set" + return @{ Status = "failed" } +} + +$_cmakeDir = [System.IO.Path]::Combine($env:TOOLS_FOLDER, "cmake@$env:TOOLS_VERSION_CMAKE") + +if (-not (Test-Path $_cmakeDir)) { + # Print status if Write-Tool is available + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "cmake" $env:TOOLS_VERSION_CMAKE "installing" + } + + # Determine platform and architecture + $_os = if ($IsWindows -or $env:OS -eq "Windows_NT") { "Windows" } elseif ($IsMacOS) { "Darwin" } else { "Linux" } + $_arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() + + switch ($_os) { + "Darwin" { + $_url = "https://github.com/Kitware/CMake/releases/download/v$env:TOOLS_VERSION_CMAKE/cmake-$env:TOOLS_VERSION_CMAKE-macos-universal.tar.gz" + $_binSubdir = "CMake.app/Contents/bin" + } + "Linux" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/Kitware/CMake/releases/download/v$env:TOOLS_VERSION_CMAKE/cmake-$env:TOOLS_VERSION_CMAKE-linux-x86_64.tar.gz" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/Kitware/CMake/releases/download/v$env:TOOLS_VERSION_CMAKE/cmake-$env:TOOLS_VERSION_CMAKE-linux-aarch64.tar.gz" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + $_binSubdir = "bin" + } + "Windows" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/Kitware/CMake/releases/download/v$env:TOOLS_VERSION_CMAKE/cmake-$env:TOOLS_VERSION_CMAKE-windows-x86_64.zip" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/Kitware/CMake/releases/download/v$env:TOOLS_VERSION_CMAKE/cmake-$env:TOOLS_VERSION_CMAKE-windows-arm64.zip" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + $_binSubdir = "bin" + } + } + + # Download and extract + New-Item -ItemType Directory -Path $_cmakeDir -Force | Out-Null + $_tempFile = [System.IO.Path]::GetTempFileName() + + try { + if ($_os -eq "Windows") { + $_tempFile = $_tempFile -replace '\.tmp$', '.zip' + Invoke-WebRequest -Uri $_url -OutFile $_tempFile -UseBasicParsing + Expand-Archive -Path $_tempFile -DestinationPath $_cmakeDir -Force + # Move contents up from nested directory + $_nested = Get-ChildItem -Path $_cmakeDir -Directory | Select-Object -First 1 + if ($_nested) { + Get-ChildItem -Path $_nested.FullName | Move-Item -Destination $_cmakeDir -Force + Remove-Item -Path $_nested.FullName -Recurse -Force + } + } else { + # Use curl and tar for macOS/Linux + $null = bash -c "curl -fsSL '$_url' | tar -xz -C '$_cmakeDir' --strip-components=1" 2>&1 + } + $_status = if (Test-Path "$_cmakeDir/$_binSubdir/cmake*") { "installed" } else { "failed" } + } catch { + Remove-Item -Path $_cmakeDir -Recurse -Force -ErrorAction SilentlyContinue + $_status = "failed" + } finally { + Remove-Item -Path $_tempFile -Force -ErrorAction SilentlyContinue + } +} else { + # Already installed + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "cmake" $env:TOOLS_VERSION_CMAKE "available" + } + $_status = "available" +} + +# Add to PATH +if ($_status -ne "failed") { + $PathSep = [System.IO.Path]::PathSeparator + # Determine bin directory based on OS + if (Test-Path "$_cmakeDir/CMake.app") { + $env:PATH = "$_cmakeDir/CMake.app/Contents/bin$PathSep$env:PATH" + } else { + $env:PATH = "$_cmakeDir/bin$PathSep$env:PATH" + } +} + +# Return result +@{ Status = $_status } diff --git a/scripts/tools/cmake.sh b/scripts/tools/cmake.sh new file mode 100644 index 0000000..29ff1fc --- /dev/null +++ b/scripts/tools/cmake.sh @@ -0,0 +1,94 @@ +# Tool script for CMake (cross-platform build system) +# Source this file after setting TOOLS_FOLDER and TOOLS_VERSION_CMAKE +# +# Requires (environment variables): +# TOOLS_FOLDER - Base folder for tools (absolute path) +# TOOLS_VERSION_CMAKE - Version to install (e.g., 4.2.1) +# +# Provides (after sourcing): +# SETUP_STATUS - "installed" | "available" | "failed" +# +# Tools made available: +# cmake, ctest, cpack +# +# Behavior: +# - Checks if cmake is already installed (early exit if yes) +# - Prints status via _print_tool (if available) +# - Downloads prebuilt binary from GitHub releases +# - Adds cmake to PATH + +# Validate required variables +if [[ -z "$TOOLS_FOLDER" ]] || [[ -z "$TOOLS_VERSION_CMAKE" ]]; then + echo "Error: TOOLS_FOLDER and TOOLS_VERSION_CMAKE must be set" >&2 + SETUP_STATUS="failed" + return 1 +fi + +_cmake_dir="$TOOLS_FOLDER/cmake@$TOOLS_VERSION_CMAKE" + +if [[ ! -d "$_cmake_dir" ]]; then + # Print status if _print_tool is available + type _print_tool &>/dev/null && _print_tool "cmake" "$TOOLS_VERSION_CMAKE" "installing" + + # Determine platform and architecture + _os="$(uname -s)" + _arch="$(uname -m)" + + case "$_os" in + Darwin) + _url="https://github.com/Kitware/CMake/releases/download/v${TOOLS_VERSION_CMAKE}/cmake-${TOOLS_VERSION_CMAKE}-macos-universal.tar.gz" + _strip=1 # Strip top-level directory + _bin_subdir="CMake.app/Contents/bin" + ;; + Linux) + case "$_arch" in + x86_64) + _url="https://github.com/Kitware/CMake/releases/download/v${TOOLS_VERSION_CMAKE}/cmake-${TOOLS_VERSION_CMAKE}-linux-x86_64.tar.gz" + ;; + aarch64|arm64) + _url="https://github.com/Kitware/CMake/releases/download/v${TOOLS_VERSION_CMAKE}/cmake-${TOOLS_VERSION_CMAKE}-linux-aarch64.tar.gz" + ;; + *) + echo "Error: Unsupported architecture: $_arch" >&2 + SETUP_STATUS="failed" + unset _cmake_dir _os _arch + return 1 + ;; + esac + _strip=1 + _bin_subdir="bin" + ;; + *) + echo "Error: Unsupported OS: $_os" >&2 + SETUP_STATUS="failed" + unset _cmake_dir _os _arch + return 1 + ;; + esac + + # Download and extract + mkdir -p "$_cmake_dir" + if curl -fsSL "$_url" | tar -xz -C "$_cmake_dir" --strip-components=$_strip 2>/dev/null; then + SETUP_STATUS="installed" + else + rm -rf "$_cmake_dir" + SETUP_STATUS="failed" + fi + + unset _url _strip _bin_subdir _os _arch +else + type _print_tool &>/dev/null && _print_tool "cmake" "$TOOLS_VERSION_CMAKE" "available" + SETUP_STATUS="available" +fi + +# Add to PATH +if [[ "$SETUP_STATUS" != "failed" ]]; then + # Determine bin directory based on OS + if [[ -d "$_cmake_dir/CMake.app" ]]; then + _export_path "$_cmake_dir/CMake.app/Contents/bin" + else + _export_path "$_cmake_dir/bin" + fi +fi + +unset _cmake_dir diff --git a/scripts/tools/emscripten.ps1 b/scripts/tools/emscripten.ps1 new file mode 100644 index 0000000..88239ad --- /dev/null +++ b/scripts/tools/emscripten.ps1 @@ -0,0 +1,123 @@ +# Tool script for Emscripten SDK (WebAssembly compiler toolchain) +# Dot-source this file after setting $env:TOOLS_FOLDER and $env:TOOLS_VERSION_EMSDK +# +# Requires (environment variables): +# $env:TOOLS_FOLDER - Base folder for tools (absolute path) +# $env:TOOLS_VERSION_EMSDK - Version to install (e.g., 4.0.9) +# +# Returns (hashtable): +# @{ Status = "installed" | "available" | "failed" } +# +# Tools made available: +# emcc, em++, emar, emranlib, emmake, emcmake, emrun, emsdk +# python, node (from emsdk bundled versions) +# +# Behavior: +# - Checks if emsdk is already installed (early exit if yes) +# - Prints status via Write-Tool (if available) +# - Clones emsdk repo and installs specified version if missing +# - Sources emsdk_env to set PATH and other env vars +# - Adds python and node from emsdk to PATH + +# Validate required variables +if (-not $env:TOOLS_FOLDER -or -not $env:TOOLS_VERSION_EMSDK) { + Write-Error "TOOLS_FOLDER and TOOLS_VERSION_EMSDK must be set" + return @{ Status = "failed" } +} + +$_emsdkDir = [System.IO.Path]::Combine($env:TOOLS_FOLDER, "emsdk@$env:TOOLS_VERSION_EMSDK") + +if (-not (Test-Path $_emsdkDir)) { + # Clone emsdk repository + git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$_emsdkDir" 2>&1 | Out-Null + + if (Test-Path $_emsdkDir) { + # Install and activate the specified version + Push-Location $_emsdkDir + try { + if ($IsWindows -or $env:OS -eq "Windows_NT") { + # Windows: use batch scripts + & cmd /c "emsdk.bat install $env:TOOLS_VERSION_EMSDK" 2>&1 | Out-Null + & cmd /c "emsdk.bat activate $env:TOOLS_VERSION_EMSDK" 2>&1 | Out-Null + } else { + # macOS/Linux: use shell scripts + bash -c "./emsdk install $env:TOOLS_VERSION_EMSDK" 2>&1 | Out-Null + bash -c "./emsdk activate $env:TOOLS_VERSION_EMSDK" 2>&1 | Out-Null + } + } finally { + Pop-Location + } + + $_status = if (Test-Path "$_emsdkDir/emsdk_env.sh") { "installed" } else { "failed" } + } else { + $_status = "failed" + } +} else { + $_status = "available" +} + +# Source emsdk_env to set up PATH and environment variables +if ($_status -ne "failed") { + if ($IsWindows -or $env:OS -eq "Windows_NT") { + # Windows: parse emsdk_env.bat output or use the .ps1 if available + $envScript = [System.IO.Path]::Combine($_emsdkDir, "emsdk_env.ps1") + if (Test-Path $envScript) { + . $envScript 2>&1 | Out-Null + } else { + # Fallback: manually add to PATH + $upstreamDir = [System.IO.Path]::Combine($_emsdkDir, "upstream", "emscripten") + if (Test-Path $upstreamDir) { + $PathSep = [System.IO.Path]::PathSeparator + $env:PATH = "$upstreamDir$PathSep$_emsdkDir$PathSep$env:PATH" + } + } + } else { + # macOS/Linux: source emsdk_env.sh and capture env changes + $envScript = [System.IO.Path]::Combine($_emsdkDir, "emsdk_env.sh") + if (Test-Path $envScript) { + # Run bash to source the script and output env vars + $envOutput = bash -c "source '$envScript' >/dev/null 2>&1 && env" + foreach ($line in $envOutput -split "`n") { + if ($line -match '^([^=]+)=(.*)$') { + $varName = $matches[1] + $varValue = $matches[2] + # Only set emsdk-related vars and PATH + if ($varName -eq 'PATH' -or $varName -like 'EMSDK*' -or $varName -eq 'EM_CONFIG') { + [Environment]::SetEnvironmentVariable($varName, $varValue, 'Process') + } + } + } + } + } +} + +# Add python and node from emsdk to PATH and print their versions (before emscripten) +if ($_status -ne "failed") { + $PathSep = [System.IO.Path]::PathSeparator + + if ($env:EMSDK_PYTHON -and (Test-Path $env:EMSDK_PYTHON)) { + $_pythonDir = Split-Path -Parent $env:EMSDK_PYTHON + $env:PATH = "$_pythonDir$PathSep$env:PATH" + $_pythonVer = ((& $env:EMSDK_PYTHON --version 2>$null) -split ' ')[1] + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "python" $_pythonVer $_status + } + } + + if ($env:EMSDK_NODE -and (Test-Path $env:EMSDK_NODE)) { + $_nodeDir = Split-Path -Parent $env:EMSDK_NODE + $env:PATH = "$_nodeDir$PathSep$env:PATH" + $_nodeVer = (& $env:EMSDK_NODE --version 2>$null) -replace '^v', '' + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "node" $_nodeVer $_status + } + } +} + +# Print emscripten status last +if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "emscripten" $env:TOOLS_VERSION_EMSDK $_status +} + +# Return result +@{ Status = $_status } diff --git a/scripts/tools/emscripten.sh b/scripts/tools/emscripten.sh new file mode 100644 index 0000000..9c48ecd --- /dev/null +++ b/scripts/tools/emscripten.sh @@ -0,0 +1,95 @@ +# Tool script for Emscripten SDK (WebAssembly compiler toolchain) +# Source this file after setting TOOLS_FOLDER and TOOLS_VERSION_EMSDK +# +# Requires (environment variables): +# TOOLS_FOLDER - Base folder for tools (absolute path) +# TOOLS_VERSION_EMSDK - Version to install (e.g., 4.0.9) +# +# Provides (after sourcing): +# SETUP_STATUS - "installed" | "available" | "failed" +# +# Tools made available: +# emcc, em++, emar, emranlib, emmake, emcmake, emrun, emsdk +# node (from emsdk bundled version) +# +# Behavior: +# - Checks if emsdk is already installed (early exit if yes) +# - Prints status via _print_tool (if available) +# - Clones emsdk repo and installs specified version if missing +# - Sources emsdk_env.sh to set PATH and other env vars +# - Adds node from emsdk to PATH (Python is managed by uv) + +# Validate required variables +if [[ -z "$TOOLS_FOLDER" ]] || [[ -z "$TOOLS_VERSION_EMSDK" ]]; then + echo "Error: TOOLS_FOLDER and TOOLS_VERSION_EMSDK must be set" >&2 + SETUP_STATUS="failed" + return 1 +fi + +_emsdk_dir="$TOOLS_FOLDER/emsdk@$TOOLS_VERSION_EMSDK" + +if [[ ! -d "$_emsdk_dir" ]]; then + # Clone emsdk repository + git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$_emsdk_dir" >/dev/null 2>&1 + + if [[ -d "$_emsdk_dir" ]]; then + # Install and activate the specified version + ( + cd "$_emsdk_dir" + ./emsdk install "$TOOLS_VERSION_EMSDK" >/dev/null 2>&1 + ./emsdk activate "$TOOLS_VERSION_EMSDK" >/dev/null 2>&1 + ) + + if [[ -f "$_emsdk_dir/emsdk_env.sh" ]]; then + SETUP_STATUS="installed" + else + SETUP_STATUS="failed" + fi + else + SETUP_STATUS="failed" + fi +else + SETUP_STATUS="available" +fi + +# Source emsdk_env.sh to set up PATH and environment variables +# This handles all the complexity of emcc, em++, emar, etc. +if [[ "$SETUP_STATUS" != "failed" ]] && [[ -f "$_emsdk_dir/emsdk_env.sh" ]]; then + # Capture PATH before sourcing + _path_before="$PATH" + source "$_emsdk_dir/emsdk_env.sh" >/dev/null 2>&1 + + # In GitHub Actions, write new PATH entries to GITHUB_PATH + if [[ -n "$GITHUB_ACTIONS" ]] && [[ -n "$GITHUB_PATH" ]]; then + # Extract new entries added by emsdk_env.sh + _new_path="${PATH%%:$_path_before}" + IFS=':' read -ra _path_entries <<< "$_new_path" + for _entry in "${_path_entries[@]}"; do + [[ -n "$_entry" ]] && echo "$_entry" >> "$GITHUB_PATH" + done + unset _new_path _path_entries _entry + + # Also export EMSDK environment variables + [[ -n "$EMSDK" ]] && echo "EMSDK=$EMSDK" >> "$GITHUB_ENV" + [[ -n "$EMSDK_NODE" ]] && echo "EMSDK_NODE=$EMSDK_NODE" >> "$GITHUB_ENV" + [[ -n "$EMSDK_PYTHON" ]] && echo "EMSDK_PYTHON=$EMSDK_PYTHON" >> "$GITHUB_ENV" + [[ -n "$EM_CONFIG" ]] && echo "EM_CONFIG=$EM_CONFIG" >> "$GITHUB_ENV" + fi + unset _path_before +fi + +# Add node from emsdk to PATH (Python is managed by uv, not emsdk) +if [[ "$SETUP_STATUS" != "failed" ]]; then + if [[ -n "$EMSDK_NODE" ]] && [[ -f "$EMSDK_NODE" ]]; then + _node_dir="$(dirname "$EMSDK_NODE")" + _export_path "$_node_dir" + _node_ver="$("$EMSDK_NODE" --version 2>/dev/null | tr -d 'v')" + type _print_tool &>/dev/null && _print_tool "node" "$_node_ver" "$SETUP_STATUS" + unset _node_dir _node_ver + fi +fi + +# Print emscripten status last +type _print_tool &>/dev/null && _print_tool "emscripten" "$TOOLS_VERSION_EMSDK" "$SETUP_STATUS" + +unset _emsdk_dir diff --git a/scripts/tools/gh.ps1 b/scripts/tools/gh.ps1 new file mode 100644 index 0000000..19be228 --- /dev/null +++ b/scripts/tools/gh.ps1 @@ -0,0 +1,133 @@ +# Tool script for gh (GitHub CLI) +# Dot-source this file after setting $env:TOOLS_FOLDER and $env:TOOLS_VERSION_GH +# +# Requires (environment variables): +# $env:TOOLS_FOLDER - Base folder for tools (absolute path) +# $env:TOOLS_VERSION_GH - Version to install (e.g., 2.83.1) +# +# Returns (hashtable): +# @{ Status = "installed" | "available" | "failed" } +# +# Tools made available: +# gh +# +# Behavior: +# - Checks if gh is already installed (early exit if yes) +# - Prints status via Write-Tool (if available) +# - Downloads prebuilt binary from GitHub releases +# - Adds gh to PATH + +# Validate required variables +if (-not $env:TOOLS_FOLDER -or -not $env:TOOLS_VERSION_GH) { + Write-Error "TOOLS_FOLDER and TOOLS_VERSION_GH must be set" + return @{ Status = "failed" } +} + +$_ghDir = [System.IO.Path]::Combine($env:TOOLS_FOLDER, "gh@$env:TOOLS_VERSION_GH") + +if (-not (Test-Path $_ghDir)) { + # Print status if Write-Tool is available + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "gh" $env:TOOLS_VERSION_GH "installing" + } + + # Determine platform and architecture + $_os = if ($IsWindows -or $env:OS -eq "Windows_NT") { "Windows" } elseif ($IsMacOS) { "Darwin" } else { "Linux" } + $_arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() + + switch ($_os) { + "Darwin" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/cli/cli/releases/download/v$env:TOOLS_VERSION_GH/gh_$($env:TOOLS_VERSION_GH)_macOS_amd64.zip" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/cli/cli/releases/download/v$env:TOOLS_VERSION_GH/gh_$($env:TOOLS_VERSION_GH)_macOS_arm64.zip" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + } + "Linux" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/cli/cli/releases/download/v$env:TOOLS_VERSION_GH/gh_$($env:TOOLS_VERSION_GH)_linux_amd64.tar.gz" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/cli/cli/releases/download/v$env:TOOLS_VERSION_GH/gh_$($env:TOOLS_VERSION_GH)_linux_arm64.tar.gz" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + } + "Windows" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/cli/cli/releases/download/v$env:TOOLS_VERSION_GH/gh_$($env:TOOLS_VERSION_GH)_windows_amd64.zip" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/cli/cli/releases/download/v$env:TOOLS_VERSION_GH/gh_$($env:TOOLS_VERSION_GH)_windows_arm64.zip" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + } + } + + # Download and extract + New-Item -ItemType Directory -Path $_ghDir -Force | Out-Null + $_isZip = $_url.EndsWith(".zip") + $_tempFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "gh-$env:TOOLS_VERSION_GH$( if ($_isZip) { '.zip' } else { '.tar.gz' } )") + $_tempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "gh-extract-$env:TOOLS_VERSION_GH") + + try { + Invoke-WebRequest -Uri $_url -OutFile $_tempFile -UseBasicParsing + + if ($_isZip) { + Expand-Archive -Path $_tempFile -DestinationPath $_tempDir -Force + # Find and copy the gh binary + $ghBin = Get-ChildItem -Path $_tempDir -Recurse -Filter "gh*" | Where-Object { $_.Name -match "^gh(\.exe)?$" } | Select-Object -First 1 + if ($ghBin) { + Copy-Item -Path $ghBin.FullName -Destination $_ghDir + } + } else { + New-Item -ItemType Directory -Path $_tempDir -Force | Out-Null + tar -xzf $_tempFile -C $_tempDir + # Find and copy the gh binary + $ghBin = Get-ChildItem -Path $_tempDir -Recurse -Filter "gh" | Select-Object -First 1 + if ($ghBin) { + Copy-Item -Path $ghBin.FullName -Destination $_ghDir + chmod +x "$_ghDir/gh" 2>$null + } + } + + $_status = "installed" + } catch { + Remove-Item -Path $_ghDir -Recurse -Force -ErrorAction SilentlyContinue + $_status = "failed" + } finally { + Remove-Item -Path $_tempFile -Force -ErrorAction SilentlyContinue + Remove-Item -Path $_tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} else { + # Already installed + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "gh" $env:TOOLS_VERSION_GH "available" + } + $_status = "available" +} + +# Add to PATH +if ($_status -ne "failed") { + $PathSep = [System.IO.Path]::PathSeparator + $env:PATH = "$_ghDir$PathSep$env:PATH" +} + +# Return result +@{ Status = $_status } diff --git a/scripts/tools/gh.sh b/scripts/tools/gh.sh new file mode 100644 index 0000000..53e2bc1 --- /dev/null +++ b/scripts/tools/gh.sh @@ -0,0 +1,119 @@ +# Tool script for gh (GitHub CLI) +# Source this file after setting TOOLS_FOLDER and TOOLS_VERSION_GH +# +# Requires (environment variables): +# TOOLS_FOLDER - Base folder for tools (absolute path) +# TOOLS_VERSION_GH - Version to install (e.g., 2.83.1) +# +# Provides (after sourcing): +# SETUP_STATUS - "installed" | "available" | "failed" +# +# Tools made available: +# gh +# +# Behavior: +# - Checks if gh is already installed (early exit if yes) +# - Prints status via _print_tool (if available) +# - Downloads prebuilt binary from GitHub releases +# - Adds gh to PATH + +# Validate required variables +if [[ -z "$TOOLS_FOLDER" ]] || [[ -z "$TOOLS_VERSION_GH" ]]; then + echo "Error: TOOLS_FOLDER and TOOLS_VERSION_GH must be set" >&2 + SETUP_STATUS="failed" + return 1 +fi + +_gh_dir="$TOOLS_FOLDER/gh@$TOOLS_VERSION_GH" + +if [[ ! -d "$_gh_dir" ]]; then + # Print status if _print_tool is available + type _print_tool &>/dev/null && _print_tool "gh" "$TOOLS_VERSION_GH" "installing" + + # Determine platform and architecture + _os="$(uname -s)" + _arch="$(uname -m)" + + case "$_os" in + Darwin) + case "$_arch" in + x86_64) + _url="https://github.com/cli/cli/releases/download/v${TOOLS_VERSION_GH}/gh_${TOOLS_VERSION_GH}_macOS_amd64.zip" + _format="zip" + ;; + arm64) + _url="https://github.com/cli/cli/releases/download/v${TOOLS_VERSION_GH}/gh_${TOOLS_VERSION_GH}_macOS_arm64.zip" + _format="zip" + ;; + *) + echo "Error: Unsupported architecture: $_arch" >&2 + SETUP_STATUS="failed" + unset _gh_dir _os _arch + return 1 + ;; + esac + ;; + Linux) + case "$_arch" in + x86_64) + _url="https://github.com/cli/cli/releases/download/v${TOOLS_VERSION_GH}/gh_${TOOLS_VERSION_GH}_linux_amd64.tar.gz" + _format="tar.gz" + ;; + aarch64|arm64) + _url="https://github.com/cli/cli/releases/download/v${TOOLS_VERSION_GH}/gh_${TOOLS_VERSION_GH}_linux_arm64.tar.gz" + _format="tar.gz" + ;; + *) + echo "Error: Unsupported architecture: $_arch" >&2 + SETUP_STATUS="failed" + unset _gh_dir _os _arch + return 1 + ;; + esac + ;; + *) + echo "Error: Unsupported OS: $_os" >&2 + SETUP_STATUS="failed" + unset _gh_dir _os _arch + return 1 + ;; + esac + + # Download and extract + mkdir -p "$_gh_dir" + _temp_file=$(mktemp) + + if curl -fsSL "$_url" -o "$_temp_file" 2>/dev/null; then + if [[ "$_format" == "zip" ]]; then + # Unzip and move contents from nested directory + _temp_dir=$(mktemp -d) + unzip -q "$_temp_file" -d "$_temp_dir" + mv "$_temp_dir"/gh_*/bin/gh "$_gh_dir/" + rm -rf "$_temp_dir" + else + # Extract tar.gz and move contents from nested directory + _temp_dir=$(mktemp -d) + tar -xzf "$_temp_file" -C "$_temp_dir" + mv "$_temp_dir"/gh_*/bin/gh "$_gh_dir/" + rm -rf "$_temp_dir" + fi + chmod +x "$_gh_dir/gh" + SETUP_STATUS="installed" + else + rm -rf "$_gh_dir" + SETUP_STATUS="failed" + fi + + rm -f "$_temp_file" + unset _url _format _os _arch _temp_file _temp_dir +else + type _print_tool &>/dev/null && _print_tool "gh" "$TOOLS_VERSION_GH" "available" + SETUP_STATUS="available" +fi + +# Add to PATH +if [[ "$SETUP_STATUS" != "failed" ]]; then + _export_path "$_gh_dir" +fi + +unset _gh_dir diff --git a/scripts/tools/ninja.ps1 b/scripts/tools/ninja.ps1 new file mode 100644 index 0000000..24d7eab --- /dev/null +++ b/scripts/tools/ninja.ps1 @@ -0,0 +1,107 @@ +# Tool script for Ninja (fast build system) +# Dot-source this file after setting $env:TOOLS_FOLDER and $env:TOOLS_VERSION_NINJA +# +# Requires (environment variables): +# $env:TOOLS_FOLDER - Base folder for tools (absolute path) +# $env:TOOLS_VERSION_NINJA - Version to install (e.g., 1.13.2) +# +# Returns (hashtable): +# @{ Status = "installed" | "available" | "failed" } +# +# Tools made available: +# ninja +# +# Behavior: +# - Checks if ninja is already installed (early exit if yes) +# - Prints status via Write-Tool (if available) +# - Downloads prebuilt binary from GitHub releases +# - Adds ninja to PATH + +# Validate required variables +if (-not $env:TOOLS_FOLDER -or -not $env:TOOLS_VERSION_NINJA) { + Write-Error "TOOLS_FOLDER and TOOLS_VERSION_NINJA must be set" + return @{ Status = "failed" } +} + +$_ninjaDir = [System.IO.Path]::Combine($env:TOOLS_FOLDER, "ninja@$env:TOOLS_VERSION_NINJA") + +if (-not (Test-Path $_ninjaDir)) { + # Print status if Write-Tool is available + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "ninja" $env:TOOLS_VERSION_NINJA "installing" + } + + # Determine platform and architecture + $_os = if ($IsWindows -or $env:OS -eq "Windows_NT") { "Windows" } elseif ($IsMacOS) { "Darwin" } else { "Linux" } + $_arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() + + switch ($_os) { + "Darwin" { + $_url = "https://github.com/ninja-build/ninja/releases/download/v$env:TOOLS_VERSION_NINJA/ninja-mac.zip" + } + "Linux" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/ninja-build/ninja/releases/download/v$env:TOOLS_VERSION_NINJA/ninja-linux.zip" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/ninja-build/ninja/releases/download/v$env:TOOLS_VERSION_NINJA/ninja-linux-aarch64.zip" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + } + "Windows" { + switch ($_arch) { + { $_ -in "x64", "x86_64" } { + $_url = "https://github.com/ninja-build/ninja/releases/download/v$env:TOOLS_VERSION_NINJA/ninja-win.zip" + } + { $_ -in "arm64", "aarch64" } { + $_url = "https://github.com/ninja-build/ninja/releases/download/v$env:TOOLS_VERSION_NINJA/ninja-winarm64.zip" + } + default { + Write-Error "Unsupported architecture: $_arch" + return @{ Status = "failed" } + } + } + } + } + + # Download and extract + New-Item -ItemType Directory -Path $_ninjaDir -Force | Out-Null + $_tempFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "ninja-$env:TOOLS_VERSION_NINJA.zip") + + try { + Invoke-WebRequest -Uri $_url -OutFile $_tempFile -UseBasicParsing + Expand-Archive -Path $_tempFile -DestinationPath $_ninjaDir -Force + + # Make executable on Unix + if ($_os -ne "Windows") { + chmod +x "$_ninjaDir/ninja" 2>$null + } + + $_status = if (Test-Path "$_ninjaDir/ninja*") { "installed" } else { "failed" } + } catch { + Remove-Item -Path $_ninjaDir -Recurse -Force -ErrorAction SilentlyContinue + $_status = "failed" + } finally { + Remove-Item -Path $_tempFile -Force -ErrorAction SilentlyContinue + } +} else { + # Already installed + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "ninja" $env:TOOLS_VERSION_NINJA "available" + } + $_status = "available" +} + +# Add to PATH (ninja binary is directly in install dir) +if ($_status -ne "failed") { + $PathSep = [System.IO.Path]::PathSeparator + $env:PATH = "$_ninjaDir$PathSep$env:PATH" +} + +# Return result +@{ Status = $_status } diff --git a/scripts/tools/ninja.sh b/scripts/tools/ninja.sh new file mode 100644 index 0000000..1a88af2 --- /dev/null +++ b/scripts/tools/ninja.sh @@ -0,0 +1,89 @@ +# Tool script for Ninja (fast build system) +# Source this file after setting TOOLS_FOLDER and TOOLS_VERSION_NINJA +# +# Requires (environment variables): +# TOOLS_FOLDER - Base folder for tools (absolute path) +# TOOLS_VERSION_NINJA - Version to install (e.g., 1.13.2) +# +# Provides (after sourcing): +# SETUP_STATUS - "installed" | "available" | "failed" +# +# Tools made available: +# ninja +# +# Behavior: +# - Checks if ninja is already installed (early exit if yes) +# - Prints status via _print_tool (if available) +# - Downloads prebuilt binary from GitHub releases +# - Adds ninja to PATH + +# Validate required variables +if [[ -z "$TOOLS_FOLDER" ]] || [[ -z "$TOOLS_VERSION_NINJA" ]]; then + echo "Error: TOOLS_FOLDER and TOOLS_VERSION_NINJA must be set" >&2 + SETUP_STATUS="failed" + return 1 +fi + +_ninja_dir="$TOOLS_FOLDER/ninja@$TOOLS_VERSION_NINJA" + +if [[ ! -d "$_ninja_dir" ]]; then + # Print status if _print_tool is available + type _print_tool &>/dev/null && _print_tool "ninja" "$TOOLS_VERSION_NINJA" "installing" + + # Determine platform and architecture + _os="$(uname -s)" + _arch="$(uname -m)" + + case "$_os" in + Darwin) + # Ninja provides universal binary for macOS + _url="https://github.com/ninja-build/ninja/releases/download/v${TOOLS_VERSION_NINJA}/ninja-mac.zip" + ;; + Linux) + case "$_arch" in + x86_64) + _url="https://github.com/ninja-build/ninja/releases/download/v${TOOLS_VERSION_NINJA}/ninja-linux.zip" + ;; + aarch64|arm64) + _url="https://github.com/ninja-build/ninja/releases/download/v${TOOLS_VERSION_NINJA}/ninja-linux-aarch64.zip" + ;; + *) + echo "Error: Unsupported architecture: $_arch" >&2 + SETUP_STATUS="failed" + unset _ninja_dir _os _arch + return 1 + ;; + esac + ;; + *) + echo "Error: Unsupported OS: $_os" >&2 + SETUP_STATUS="failed" + unset _ninja_dir _os _arch + return 1 + ;; + esac + + # Download and extract (ninja releases are .zip files with just the binary inside) + mkdir -p "$_ninja_dir" + _temp_zip=$(mktemp) + if curl -fsSL "$_url" -o "$_temp_zip" && unzip -q "$_temp_zip" -d "$_ninja_dir" 2>/dev/null; then + chmod +x "$_ninja_dir/ninja" + SETUP_STATUS="installed" + else + rm -rf "$_ninja_dir" + SETUP_STATUS="failed" + fi + rm -f "$_temp_zip" + + unset _url _temp_zip _os _arch +else + type _print_tool &>/dev/null && _print_tool "ninja" "$TOOLS_VERSION_NINJA" "available" + SETUP_STATUS="available" +fi + +# Add to PATH (ninja binary is directly in install dir) +if [[ "$SETUP_STATUS" != "failed" ]]; then + _export_path "$_ninja_dir" +fi + +unset _ninja_dir diff --git a/scripts/tools/uv.ps1 b/scripts/tools/uv.ps1 new file mode 100644 index 0000000..9c4c1e2 --- /dev/null +++ b/scripts/tools/uv.ps1 @@ -0,0 +1,61 @@ +# Tool script for uv (Python package manager) +# Dot-source this file after setting $env:TOOLS_FOLDER and $env:TOOLS_VERSION_UV +# +# Requires (environment variables): +# $env:TOOLS_FOLDER - Base folder for tools (absolute path) +# $env:TOOLS_VERSION_UV - Version to install (e.g., 0.5.14) +# +# Returns (hashtable): +# @{ Status = "installed" | "available" | "failed" } +# +# Behavior: +# - Checks if tool is already installed (early exit if yes) +# - Prints status via Write-Tool (if available) +# - Downloads and installs tool if missing +# - Adds tool to PATH + +# Validate required variables +if (-not $env:TOOLS_FOLDER -or -not $env:TOOLS_VERSION_UV) { + Write-Error "TOOLS_FOLDER and TOOLS_VERSION_UV must be set" + return @{ Status = "failed" } +} + +$_uvDir = [System.IO.Path]::Combine($env:TOOLS_FOLDER, "uv@$env:TOOLS_VERSION_UV") + +if (-not (Test-Path $_uvDir)) { + # Print status if Write-Tool is available + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "uv" $env:TOOLS_VERSION_UV "installing" + } + + # Install uv + $env:UV_INSTALL_DIR = $_uvDir + $env:UV_UNMANAGED_INSTALL = "1" + + if ($IsWindows -or $env:OS -eq "Windows_NT") { + # Windows: use PowerShell installer + irm "https://astral.sh/uv/$env:TOOLS_VERSION_UV/install.ps1" | iex *>&1 | Out-Null + } else { + # macOS/Linux: use shell installer via sh + sh -c "curl -LsSf https://astral.sh/uv/$env:TOOLS_VERSION_UV/install.sh | sh" 2>&1 | Out-Null + } + + Remove-Item Env:\UV_UNMANAGED_INSTALL -ErrorAction SilentlyContinue + + $_status = if (Test-Path $_uvDir) { "installed" } else { "failed" } +} else { + # Already installed + if (Get-Command Write-Tool -ErrorAction SilentlyContinue) { + Write-Tool "uv" $env:TOOLS_VERSION_UV "available" + } + $_status = "available" +} + +# Add to PATH (uv binary is directly in install dir) +if ($_status -ne "failed") { + $PathSep = [System.IO.Path]::PathSeparator + $env:PATH = "$_uvDir$PathSep$env:PATH" +} + +# Return result +@{ Status = $_status } diff --git a/scripts/tools/uv.sh b/scripts/tools/uv.sh new file mode 100644 index 0000000..b754d3b --- /dev/null +++ b/scripts/tools/uv.sh @@ -0,0 +1,68 @@ +# Tool script for uv (Python package manager) +# Source this file after setting TOOLS_FOLDER and TOOLS_VERSION_UV +# +# Requires (environment variables): +# TOOLS_FOLDER - Base folder for tools (absolute path) +# TOOLS_VERSION_UV - Version to install (e.g., 0.5.14) +# TOOLS_PYTHON313 - Python 3.13 version to install (e.g., 3.13) +# +# Provides (after sourcing): +# SETUP_STATUS - "installed" | "available" | "failed" +# +# Behavior: +# - Checks if tool is already installed (early exit if yes) +# - Prints status via _print_tool (if available) +# - Downloads and installs tool if missing +# - Installs Python 3.13 via uv if TOOLS_PYTHON313 is set +# - Adds tool to PATH + +# Validate required variables +if [[ -z "$TOOLS_FOLDER" ]] || [[ -z "$TOOLS_VERSION_UV" ]]; then + echo "Error: TOOLS_FOLDER and TOOLS_VERSION_UV must be set" >&2 + SETUP_STATUS="failed" + return 1 +fi + +_uv_dir="$TOOLS_FOLDER/uv@$TOOLS_VERSION_UV" + +if [[ ! -d "$_uv_dir" ]]; then + # Print status if _print_tool is available + type _print_tool &>/dev/null && _print_tool "uv" "$TOOLS_VERSION_UV" "installing" + + # Install uv (must export vars for the installer subshell to see them) + export UV_INSTALL_DIR="$_uv_dir" + export UV_UNMANAGED_INSTALL="1" + curl -LsSf "https://astral.sh/uv/$TOOLS_VERSION_UV/install.sh" | sh >/dev/null 2>&1 + unset UV_UNMANAGED_INSTALL + + if [[ -d "$_uv_dir" ]]; then + SETUP_STATUS="installed" + else + SETUP_STATUS="failed" + fi +else + type _print_tool &>/dev/null && _print_tool "uv" "$TOOLS_VERSION_UV" "available" + SETUP_STATUS="available" +fi + +# Add to PATH (uv binary is directly in install dir) +if [[ "$SETUP_STATUS" != "failed" ]]; then + _export_path "$_uv_dir" +fi + +# Install Python 3.13 if TOOLS_PYTHON313 is set +if [[ "$SETUP_STATUS" != "failed" ]] && [[ -n "$TOOLS_PYTHON313" ]]; then + # Check if Python is already installed + _python_installed=$("$_uv_dir/uv" python list --only-installed 2>/dev/null | grep -c "cpython-$TOOLS_PYTHON313" || true) + if [[ "$_python_installed" -eq 0 ]]; then + "$_uv_dir/uv" python install "$TOOLS_PYTHON313" >/dev/null 2>&1 + fi + # Get installed Python version for display + _python_ver=$("$_uv_dir/uv" python list --only-installed 2>/dev/null | grep "cpython-$TOOLS_PYTHON313" | head -1 | awk '{print $1}' | sed 's/cpython-//') + if [[ -n "$_python_ver" ]]; then + type _print_tool &>/dev/null && _print_tool "python" "$_python_ver" "$SETUP_STATUS" + fi + unset _python_installed _python_ver +fi + +unset _uv_dir diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ea59054 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1311 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[manifest] +members = [ + "gpu-api", + "gpu-canvas", + "gpu-dawn", + "gpu-pyodide", + "gpu-wesl", + "gpu-wgpu", + "gpu-workspace", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "ansimarkup" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/57/2ca7c2ab234a01ac778b22fed926e7a63194699b4c1695517653723ae85f/ansimarkup-1.5.0.tar.gz", hash = "sha256:96c65d75bbed07d3dcbda8dbede8c2252c984f90d0ca07434b88a6bbf345fad3", size = 13959, upload-time = "2021-01-27T15:37:21.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/3ad81e40d752ef51a9a8c320c9385de0d98a4dad68c0e4f793befc610f56/ansimarkup-1.5.0-py2.py3-none-any.whl", hash = "sha256:3146ca74af5f69e48a9c3d41b31085c0d6378f803edeb364856d37c11a684acf", size = 11124, upload-time = "2021-01-27T15:37:20.308Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "auditwheel-emscripten" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "leb128", marker = "python_full_version >= '3.13'" }, + { name = "packaging", marker = "python_full_version >= '3.13'" }, + { name = "pyodide-cli", marker = "python_full_version >= '3.13'" }, + { name = "typer", marker = "python_full_version >= '3.13'" }, + { name = "wheel", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/67/e4632fc79fab6f4c119ee39c088199c7f5c52f7a9476d9a3a8199290fd88/auditwheel_emscripten-0.2.0.tar.gz", hash = "sha256:7265c26cd2638abafeba4b3c54d30511f6f777ce710df768b35aae9b38f92863", size = 5835953, upload-time = "2025-05-29T13:58:26.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/2c/f22685b2242175a1bae6d1f3492c666ce3662f5726f0e0be0002cb24bad5/auditwheel_emscripten-0.2.0-py3-none-any.whl", hash = "sha256:63942f224fbe06f4ce6c569ff0e4d37d1dbe826004b14bf1f08cd63ec7394417", size = 32622, upload-time = "2025-05-29T13:58:24.691Z" }, +] + +[[package]] +name = "basedpyright" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/d7/9476af6f45a70e8d23045ec59d99c2698513b7395283cadc75caeeea2b83/basedpyright-1.37.0.tar.gz", hash = "sha256:affbffced97a04a08bfc44aef2da43951a5ab5e2e55921a144ed786c4fd2c6ad", size = 22837441, upload-time = "2026-01-04T09:59:32.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/63/753918f0bad07a1b24755a540b64bca1388322615025d4c954e3740fcdbe/basedpyright-1.37.0-py3-none-any.whl", hash = "sha256:261a02a8732a19f3f585e2940582147560058626a062a2320724de84fb2dc41b", size = 11884509, upload-time = "2026-01-04T09:59:35.997Z" }, +] + +[[package]] +name = "bashlex" +version = "0.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/60/aae0bb54f9af5e0128ba90eb83d8d0d506ee8f0475c4fdda3deeda20b1d2/bashlex-0.18.tar.gz", hash = "sha256:5bb03a01c6d5676338c36fd1028009c8ad07e7d61d8a1ce3f513b7fff52796ee", size = 68742, upload-time = "2023-01-18T15:21:26.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/be/6985abb1011fda8a523cfe21ed9629e397d6e06fb5bae99750402b25c95b/bashlex-0.18-py2.py3-none-any.whl", hash = "sha256:91d73a23a3e51711919c1c899083890cdecffc91d8c088942725ac13e9dcfffa", size = 69539, upload-time = "2023-01-18T15:21:24.167Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "build" +version = "1.2.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and os_name == 'nt'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pyproject-hooks", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "cibuildwheel" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bashlex", marker = "python_full_version >= '3.11'" }, + { name = "bracex", marker = "python_full_version >= '3.11'" }, + { name = "build", marker = "python_full_version >= '3.11'" }, + { name = "certifi", marker = "python_full_version >= '3.11'" }, + { name = "dependency-groups", marker = "python_full_version >= '3.11'" }, + { name = "filelock", marker = "python_full_version >= '3.11'" }, + { name = "humanize", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "patchelf", marker = "(python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'darwin') or (python_full_version >= '3.11' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "platformdirs", marker = "python_full_version >= '3.11'" }, + { name = "pyelftools", marker = "python_full_version >= '3.11'" }, + { name = "wheel", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/8c/61b0cca8be7eef6af32b0c0e62f78dc8c91572ea8870bc6c4d061f259832/cibuildwheel-3.3.1.tar.gz", hash = "sha256:ae6eafe6f7ed3bab38919e08bf3eb92085f6387c5f7a746b40cc4775a8462f9a", size = 358374, upload-time = "2026-01-05T19:58:17.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/5c/3b6689ae5c8a720c275f9832c2d1611a2a1f445bfb16701a13f5f604af5c/cibuildwheel-3.3.1-py3-none-any.whl", hash = "sha256:6d3c387e77c5850819294863eeee4e57fb7e8ecdf87b7412e763222c16e26424", size = 127164, upload-time = "2026-01-05T19:58:16.338Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.13' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[[package]] +name = "dependency-groups" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/55/f054de99871e7beb81935dea8a10b90cd5ce42122b1c3081d5282fdb3621/dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd", size = 10093, upload-time = "2025-05-02T00:34:29.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "duty" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "failprint" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/45/e6d0209ebec8dadef28389b8de33dc0c95919f4794a0117e829dbc526631/duty-1.6.3.tar.gz", hash = "sha256:006f09863b8550ea62b0f8a4a52591dc61a82abe9c2b1c0407f8ba55c9c67f18", size = 130370, upload-time = "2025-09-19T10:05:42.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/97/771b4b3e7b1994d08517f0509f52313cd6021e41e705e6c7ca2962fcbfb3/duty-1.6.3-py3-none-any.whl", hash = "sha256:30a58bac94cdbdfddb91e28ed4e805cb0e4f965b59783cab6ebaef4d8defb3f4", size = 120704, upload-time = "2025-09-19T10:05:41.472Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "failprint" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansimarkup" }, + { name = "jinja2" }, + { name = "ptyprocess", marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/28/043c32004d2e8b5aa8f1c27d89f9e0fd09e07e78d73b896b22677109b87c/failprint-1.0.6.tar.gz", hash = "sha256:f88e8e4764de43e15416ce96cfef1a8f24ff9bcc58b3e309a1ba7eccbc45ccaf", size = 47747, upload-time = "2025-08-15T13:48:56.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/97/52f177e15bde8fa4ab8e2d4e928496b6f847524632f649625056922ff333/failprint-1.0.6-py3-none-any.whl", hash = "sha256:400a7fd3c272e1ce0b768842cafd291568fbb8a1fbc176bbd9813707d869f9a6", size = 21398, upload-time = "2025-08-15T13:48:55.166Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, +] + +[[package]] +name = "gpu-api" +version = "0.0.0" +source = { editable = "packages/gpu-api" } + +[[package]] +name = "gpu-canvas" +version = "0.0.0" +source = { editable = "packages/gpu-canvas" } + +[[package]] +name = "gpu-dawn" +version = "0.0.0" +source = { editable = "packages/gpu-dawn" } + +[[package]] +name = "gpu-pyodide" +version = "0.0.0" +source = { editable = "packages/gpu-pyodide" } + +[[package]] +name = "gpu-wesl" +version = "0.0.0" +source = { editable = "packages/gpu-wesl" } + +[[package]] +name = "gpu-wgpu" +version = "0.0.0" +source = { editable = "packages/gpu-wgpu" } + +[[package]] +name = "gpu-workspace" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "gpu-api" }, + { name = "gpu-canvas" }, + { name = "gpu-dawn" }, + { name = "gpu-wesl" }, + { name = "gpu-wgpu" }, +] + +[package.dev-dependencies] +dev = [ + { name = "basedpyright" }, + { name = "cibuildwheel", marker = "python_full_version >= '3.11'" }, + { name = "duty" }, + { name = "pyodide-build", extra = ["test"], marker = "python_full_version >= '3.13'" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "gpu-api", editable = "packages/gpu-api" }, + { name = "gpu-canvas", editable = "packages/gpu-canvas" }, + { name = "gpu-dawn", editable = "packages/gpu-dawn" }, + { name = "gpu-wesl", editable = "packages/gpu-wesl" }, + { name = "gpu-wgpu", editable = "packages/gpu-wgpu" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "basedpyright", specifier = ">=1.29" }, + { name = "cibuildwheel", marker = "python_full_version >= '3.11'", specifier = ">=3.3.1" }, + { name = "duty", specifier = ">=1.0" }, + { name = "pyodide-build", extras = ["test"], marker = "python_full_version >= '3.13'", specifier = ">=0.30.9" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "ruff", specifier = ">=0.14.10" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.13'" }, + { name = "h11", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.13'" }, + { name = "certifi", marker = "python_full_version >= '3.13'" }, + { name = "httpcore", marker = "python_full_version >= '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "leb128" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/3b/476c8bcb181abb060e45bca5ce9b5ba055ea9e2ed3fac6c25b2fe7b9f16b/leb128-1.0.8.tar.gz", hash = "sha256:3a52dca242f93f87a3d766380a93a3fad53ef4044f03018d21705d3b2d9021ee", size = 3361, upload-time = "2024-06-28T05:48:31.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/97/45ab4bab1a89e6fdbc822f818bb18b39eed0dd7ed1faac8b89bc6b49a9ed/leb128-1.0.8-py3-none-any.whl", hash = "sha256:76cd271e75ea91aa2fbf7783d60cb7d667b62143d544bcee59159ff258bf4523", size = 3778, upload-time = "2024-06-28T05:48:29.398Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "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 = "nodejs-wheel-binaries" +version = "24.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/35/d806c2ca66072e36dc340ccdbeb2af7e4f1b5bcc33f1481f00ceed476708/nodejs_wheel_binaries-24.12.0.tar.gz", hash = "sha256:f1b50aa25375e264697dec04b232474906b997c2630c8f499f4caf3692938435", size = 8058, upload-time = "2025-12-11T21:12:26.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/3b/9d6f044319cd5b1e98f07c41e2465b58cadc1c9c04a74c891578f3be6cb5/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:7564ddea0a87eff34e9b3ef71764cc2a476a8f09a5cccfddc4691148b0a47338", size = 55125859, upload-time = "2025-12-11T21:11:58.132Z" }, + { url = "https://files.pythonhosted.org/packages/48/a5/f5722bf15c014e2f476d7c76bce3d55c341d19122d8a5d86454db32a61a4/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:8ff929c4669e64613ceb07f5bbd758d528c3563820c75d5de3249eb452c0c0ab", size = 55309035, upload-time = "2025-12-11T21:12:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/a9/61/68d39a6f1b5df67805969fd2829ba7e80696c9af19537856ec912050a2be/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6ebacefa8891bc456ad3655e6bce0af7e20ba08662f79d9109986faeb703fd6f", size = 59661017, upload-time = "2025-12-11T21:12:05.268Z" }, + { url = "https://files.pythonhosted.org/packages/16/a1/31aad16f55a5e44ca7ea62d1367fc69f4b6e1dba67f58a0a41d0ed854540/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3292649a03682ccbfa47f7b04d3e4240e8c46ef04dc941b708f20e4e6a764f75", size = 60159770, upload-time = "2025-12-11T21:12:08.696Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5e/b7c569aa1862690ca4d4daf3a64cafa1ea6ce667a9e3ae3918c56e127d9b/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7fb83df312955ea355ba7f8cbd7055c477249a131d3cb43b60e4aeb8f8c730b1", size = 61653561, upload-time = "2025-12-11T21:12:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/71/87/567f58d7ba69ff0208be849b37be0f2c2e99c69e49334edd45ff44f00043/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2473c819448fedd7b036dde236b09f3c8bbf39fbbd0c1068790a0498800f498b", size = 62238331, upload-time = "2025-12-11T21:12:16.143Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9d/c6492188ce8de90093c6755a4a63bb6b2b4efb17094cb4f9a9a49c73ed3b/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_amd64.whl", hash = "sha256:2090d59f75a68079fabc9b86b14df8238b9aecb9577966dc142ce2a23a32e9bb", size = 41342076, upload-time = "2025-12-11T21:12:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/cd3290a647df567645353feed451ef4feaf5844496ced69c4dcb84295ff4/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_arm64.whl", hash = "sha256:d0c2273b667dd7e3f55e369c0085957b702144b1b04bfceb7ce2411e58333757", size = 39048104, upload-time = "2025-12-11T21:12:23.495Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "patchelf" +version = "0.17.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/a3/fdd3fa938c864aa2f11dd0b7f08befeda983d2dcdee44da493c6977a653f/patchelf-0.17.2.4.tar.gz", hash = "sha256:970ee5cd8af33e5ea2099510b2f9013fa1b8d5cd763bf3fd3961281c18101a09", size = 149629, upload-time = "2025-07-23T21:16:32.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/a7/8c4f86c78ec03db954d05fd9c57a114cc3a172a2d3e4a8b949cd5ff89471/patchelf-0.17.2.4-py3-none-macosx_10_9_universal2.whl", hash = "sha256:343bb1b94e959f9070ca9607453b04390e36bbaa33c88640b989cefad0aa049e", size = 184436, upload-time = "2025-07-23T21:16:20.578Z" }, + { url = "https://files.pythonhosted.org/packages/7e/19/f7821ef31aab01fa7dc8ebe697ece88ec4f7a0fdd3155dab2dfee4b00e5c/patchelf-0.17.2.4-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d9b35ebfada70c02679ad036407d9724ffe1255122ba4ac5e4be5868618a5689", size = 482846, upload-time = "2025-07-23T21:16:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/107fea848ecfd851d473b079cab79107487d72c4c3cdb25b9d2603a24ca2/patchelf-0.17.2.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2931a1b5b85f3549661898af7bf746afbda7903c7c9a967cfc998a3563f84fad", size = 477811, upload-time = "2025-07-23T21:16:25.145Z" }, + { url = "https://files.pythonhosted.org/packages/89/a9/a9a2103e159fd65bffbc21ecc5c8c36e44eb34fe53b4ef85fb6d08c2a635/patchelf-0.17.2.4-py3-none-manylinux2014_armv7l.manylinux_2_17_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ae44cb3c857d50f54b99e5697aa978726ada33a8a6129d4b8b7ffd28b996652d", size = 431226, upload-time = "2025-07-23T21:16:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/897d612f6df7cfd987bdf668425127efeff8d8e4ad8bfbab1c69d2a0d861/patchelf-0.17.2.4-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:680a266a70f60a7a4f4c448482c5bdba80cc8e6bb155a49dcc24238ba49927b0", size = 540276, upload-time = "2025-07-23T21:16:27.983Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b8/2b92d11533482bac9ee989081d6880845287751b5f528adbd6bb27667fbd/patchelf-0.17.2.4-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.musllinux_1_1_s390x.whl", hash = "sha256:d842b51f0401460f3b1f3a3a67d2c266a8f515a5adfbfa6e7b656cb3ac2ed8bc", size = 596632, upload-time = "2025-07-23T21:16:29.253Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/975d4bdb418f942b53e6187b95bd9e0d5e0488b7bc214685a1e43e2c2751/patchelf-0.17.2.4-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:7076d9e127230982e20a81a6e2358d3343004667ba510d9f822d4fdee29b0d71", size = 508281, upload-time = "2025-07-23T21:16:30.865Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "python_full_version >= '3.13'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pyelftools" +version = "0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/ab/33968940b2deb3d92f5b146bc6d4009a5f95d1d06c148ea2f9ee965071af/pyelftools-0.32.tar.gz", hash = "sha256:6de90ee7b8263e740c8715a925382d4099b354f29ac48ea40d840cf7aa14ace5", size = 15047199, upload-time = "2025-02-19T14:20:05.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/43/700932c4f0638c3421177144a2e86448c0d75dbaee2c7936bda3f9fd0878/pyelftools-0.32-py3-none-any.whl", hash = "sha256:013df952a006db5e138b1edf6d8a68ecc50630adbd0d83a2d41e7f846163d738", size = 188525, upload-time = "2025-02-19T14:19:59.919Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyodide-build" +version = "0.30.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "auditwheel-emscripten", marker = "python_full_version >= '3.13'" }, + { name = "build", marker = "python_full_version >= '3.13'" }, + { name = "packaging", marker = "python_full_version >= '3.13'" }, + { name = "platformdirs", marker = "python_full_version >= '3.13'" }, + { name = "pydantic", marker = "python_full_version >= '3.13'" }, + { name = "pyodide-cli", marker = "python_full_version >= '3.13'" }, + { name = "pyodide-lock", marker = "python_full_version >= '3.13'" }, + { name = "requests", marker = "python_full_version >= '3.13'" }, + { name = "resolvelib", marker = "python_full_version >= '3.13'" }, + { name = "rich", marker = "python_full_version >= '3.13'" }, + { name = "ruamel-yaml", marker = "python_full_version >= '3.13'" }, + { name = "typer", marker = "python_full_version >= '3.13'" }, + { name = "unearth", marker = "python_full_version >= '3.13'" }, + { name = "virtualenv", marker = "python_full_version >= '3.13'" }, + { name = "wheel", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/87/9c138b2cebd93cf43337b5d2fa696d359703b437afa6fe3ce30974a6b1cc/pyodide_build-0.30.9.tar.gz", hash = "sha256:acc5107a5aa1f3ef9a4d239f6747b7f5383ab892067385da1ccdd654b977189f", size = 106231, upload-time = "2025-11-12T07:29:40.15Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/1d/60c32d79cee429d8c3c073588270b7e1317d03cca3a065663914a0753df3/pyodide_build-0.30.9-py3-none-any.whl", hash = "sha256:63fcdf8ee0eb506e2c44d5cbdad97d742af35c470e3631a6799094cdb5569f5c", size = 115392, upload-time = "2025-11-12T07:29:38.422Z" }, +] + +[package.optional-dependencies] +test = [ + { name = "pytest", marker = "python_full_version >= '3.13'" }, + { name = "pytest-cov", marker = "python_full_version >= '3.13'" }, + { name = "pytest-httpserver", marker = "python_full_version >= '3.13'" }, + { name = "types-requests", marker = "python_full_version >= '3.13'" }, +] + +[[package]] +name = "pyodide-cli" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich", marker = "python_full_version >= '3.13'" }, + { name = "typer", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/2c/dae71066d7b9fac85f848e17e73c34cc6aca7ed6c6d79a3c3b231509c725/pyodide_cli-0.4.0.tar.gz", hash = "sha256:828e0b73cbd810414a61253cf0f0746a2efc8434567c57fbb03308b6d955e958", size = 12726, upload-time = "2025-09-12T09:36:13.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/15/f2db8891a579d8ed2b732be44a17ae93b51e8d2e097f004efc52713384c8/pyodide_cli-0.4.0-py3-none-any.whl", hash = "sha256:2897bf7eb00b760e0db1a2fa8dc237ac2ea793d90e0ac5244ad3f5a55d0bb417", size = 12867, upload-time = "2025-09-12T09:36:12.325Z" }, +] + +[[package]] +name = "pyodide-lock" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/65/6afbf741537a4970fff158f1e0e4a2f1a18c013bfdefc8f25fdc811e7a36/pyodide_lock-0.1.0.tar.gz", hash = "sha256:0163716ac4c7be462d7948ea4db7f824d934c1dfc609a8acb20369032c6716f3", size = 47540, upload-time = "2025-08-17T00:30:46.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8c/be629ee27c1559955dc81194c05cb709c8baa7a7acff7d34a1ae2e694e4b/pyodide_lock-0.1.0-py3-none-any.whl", hash = "sha256:fc900d61aeadb507cfe5bdf35e7bcd2d933955ea7a0f2f2cff0abf5b58225ec9", size = 9914, upload-time = "2025-08-17T00:30:39.288Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", marker = "python_full_version >= '3.13'" }, + { name = "pluggy", marker = "python_full_version >= '3.13'" }, + { name = "pytest", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-httpserver" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870, upload-time = "2025-04-10T08:17:15.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000, upload-time = "2025-04-10T08:17:13.906Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.13'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.13'" }, + { name = "urllib3", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "resolvelib" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/14/4669927e06631070edb968c78fdb6ce8992e27c9ab2cde4b3993e22ac7af/resolvelib-1.2.1.tar.gz", hash = "sha256:7d08a2022f6e16ce405d60b68c390f054efcfd0477d4b9bd019cc941c28fad1c", size = 24575, upload-time = "2025-10-11T01:07:44.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/23/c941a0d0353681ca138489983c4309e0f5095dfd902e1357004f2357ddf2/resolvelib-1.2.1-py3-none-any.whl", hash = "sha256:fb06b66c8da04172d9e72a21d7d06186d8919e32ae5ab5cdf5b9d920be805ac2", size = 18737, upload-time = "2025-10-11T01:07:43.081Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", marker = "python_full_version >= '3.13'" }, + { name = "pygments", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.18.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "python_full_version >= '3.13' and python_full_version < '3.15' and platform_python_implementation == 'CPython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5a/4ab767cd42dcd65b83c323e1620d7c01ee60a52f4032fb7b61501f45f5c2/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03", size = 147454, upload-time = "2025-11-16T16:13:02.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/184173ac1e74fd35d308108bcbf83904d6ef8439c70763189225a166b238/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77", size = 132467, upload-time = "2025-11-16T16:13:03.539Z" }, + { url = "https://files.pythonhosted.org/packages/49/1b/2d2077a25fe682ae335007ca831aff42e3cbc93c14066675cf87a6c7fc3e/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614", size = 693454, upload-time = "2025-11-16T20:22:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/90/16/e708059c4c429ad2e33be65507fc1730641e5f239fb2964efc1ba6edea94/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3", size = 700345, upload-time = "2025-11-16T16:13:04.771Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/0e8ef51df1f0950300541222e3332f20707a9c210b98f981422937d1278c/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862", size = 731306, upload-time = "2025-11-16T16:13:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f4/2cdb54b142987ddfbd01fc45ac6bd882695fbcedb9d8bbf796adc3fc3746/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d", size = 692415, upload-time = "2025-11-16T16:13:07.465Z" }, + { url = "https://files.pythonhosted.org/packages/a0/07/40b5fc701cce8240a3e2d26488985d3bbdc446e9fe397c135528d412fea6/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6", size = 705007, upload-time = "2025-11-16T20:22:42.856Z" }, + { url = "https://files.pythonhosted.org/packages/82/19/309258a1df6192fb4a77ffa8eae3e8150e8d0ffa56c1b6fa92e450ba2740/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed", size = 723974, upload-time = "2025-11-16T16:13:08.72Z" }, + { url = "https://files.pythonhosted.org/packages/67/3a/d6ee8263b521bfceb5cd2faeb904a15936480f2bb01c7ff74a14ec058ca4/ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f", size = 102836, upload-time = "2025-11-16T16:13:10.27Z" }, + { url = "https://files.pythonhosted.org/packages/ed/03/92aeb5c69018387abc49a8bb4f83b54a0471d9ef48e403b24bac68f01381/ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd", size = 121917, upload-time = "2025-11-16T16:13:12.145Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, + { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, + { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, + { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, + { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, + { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, + { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typer" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version >= '3.13'" }, + { name = "rich", marker = "python_full_version >= '3.13'" }, + { name = "shellingham", marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/30/ff9ede605e3bd086b4dd842499814e128500621f7951ca1e5ce84bbf61b1/typer-0.21.0.tar.gz", hash = "sha256:c87c0d2b6eee3b49c5c64649ec92425492c14488096dfbc8a0c2799b2f6f9c53", size = 106781, upload-time = "2025-12-25T09:54:53.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e4/5ebc1899d31d2b1601b32d21cfb4bba022ae6fce323d365f0448031b1660/typer-0.21.0-py3-none-any.whl", hash = "sha256:c79c01ca6b30af9fd48284058a7056ba0d3bf5cf10d0ff3d0c5b11b68c258ac6", size = 47109, upload-time = "2025-12-25T09:54:51.918Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "unearth" +version = "0.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "python_full_version >= '3.13'" }, + { name = "packaging", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/1f/cdad555c0e8643232cce619e8d88d5bec81b4d41e4cc1c65bee8a51a4750/unearth-0.18.2.tar.gz", hash = "sha256:1e53d7f52f46dd5f875e77ff1c55b12477e215a092e4b66c9764a77df4a9b520", size = 285169, upload-time = "2025-12-23T06:40:32.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a8/13d4e8bbfa076493f56b5c8f3a850078ee05ebfb84b808e146eec781199b/unearth-0.18.2-py3-none-any.whl", hash = "sha256:31fd55d67c0e46a1ebb78993a2010568e6c4231334a3207d18d5d4a549d8d692", size = 48039, upload-time = "2025-12-23T06:40:31.006Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib", marker = "python_full_version >= '3.13'" }, + { name = "filelock", marker = "python_full_version >= '3.13'" }, + { name = "platformdirs", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, +] diff --git a/vscode.bat b/vscode.bat new file mode 100644 index 0000000..04d5192 --- /dev/null +++ b/vscode.bat @@ -0,0 +1,6 @@ +@echo off +REM Double-click this file to open VS Code +REM The integrated terminal will automatically source activate.ps1 + +cd /d "%~dp0" +code . diff --git a/vscode.command b/vscode.command new file mode 100755 index 0000000..6da9ca5 --- /dev/null +++ b/vscode.command @@ -0,0 +1,6 @@ +#!/bin/zsh +# Double-click this file to open VS Code +# The integrated terminal will automatically source activate.sh + +cd "$(dirname "$0")" +open -a "Visual Studio Code" .