Initial commit
All checks were successful
Build Vast.ai Ollama Benchmark Image / Build and Push (push) Successful in 4m43s

This commit is contained in:
Tom Foster 2025-07-28 16:58:21 +01:00
commit 86e9de9e75
25 changed files with 4367 additions and 0 deletions

26
.env.example Normal file
View file

@ -0,0 +1,26 @@
# Vast.ai API configuration
VAST_API_KEY="YOUR_API_KEY" # Your Vast.ai API key for instance management
# GPU instance specifications
GPU_TYPE="RTX 5090" # GPU type to request (e.g. RTX 4090, RTX 5090, A100)
NUM_GPUS=1 # Number of GPUs to allocate for the instance
DISK_SPACE=30 # Disk space in GB for the instance
REGION="" # Region to filter for instances (e.g. "PL", "RO", or leave empty for any)
# Benchmark configuration
TEST_ITERATIONS=3 # Number of test runs per context length
CONTEXT_START=8000 # Starting context length for benchmark
CONTEXT_END=128000 # Maximum context length for benchmark
CONTEXT_MULTIPLIER=2 # Multiplier for context length progression
# Ollama configuration
OLLAMA_MODEL="hf.co/unsloth/gemma-3-12b-it-GGUF:Q5_K_XL" # Model to benchmark
OLLAMA_FLASH_ATTENTION="1" # Enable flash attention for performance
OLLAMA_KEEP_ALIVE="10m" # How long to keep model loaded in memory
OLLAMA_KV_CACHE_TYPE="q8_0" # KV cache quantisation type
OLLAMA_MAX_LOADED_MODELS="1" # Maximum number of loaded models
OLLAMA_NUM_GPU="1" # Number of GPUs to use for Ollama
OLLAMA_NUM_PARALLEL="1" # Number of parallel requests
# Local file transfer settings
LOCAL_RESULTS_DIR="./results" # Local directory to store benchmark results

View file

@ -0,0 +1,50 @@
name: Build Vast.ai Ollama Benchmark Image
on:
push:
pull_request:
env:
REGISTRY: git.tomfos.tr
jobs:
build-and-push:
name: Build and Push
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: https://github.com/actions/checkout@v4
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: https://github.com/docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ gitea.actor }}
password: ${{ secrets.FORGEJO_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: https://github.com/docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ gitea.repository }}
tags: |
type=ref,event=branch
type=raw,value=branch,suffix=-{{sha}},enable={{is_default_branch}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.ollama
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository }}:build-cache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository }}:build-cache,mode=max

57
.github/workflows/docker-build.yml vendored Normal file
View file

@ -0,0 +1,57 @@
name: Build and Push Docker Image
on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
name: 🚀 GPU Container Delivery
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: 📦 Checkout repository
uses: actions/checkout@v4
- name: 🐋 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔐 Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🏷️ Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: 🔨 Build and push GPU benchmarking image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.ollama
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

49
.gitignore vendored Normal file
View file

@ -0,0 +1,49 @@
# Byte-compiled / optimised / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.mypy_cache/

31
Dockerfile.ollama Normal file
View file

@ -0,0 +1,31 @@
FROM ollama/ollama:latest
# Install uv and additional utilities
RUN apt-get update && apt-get install -y \
apt-utils \
curl \
openssh-server \
tmux \
git \
wget \
less \
locales \
sudo \
software-properties-common \
rsync \
&& rm -rf /var/lib/apt/lists/* \
&& curl -LsSf https://astral.sh/uv/install.sh | sh
# Set path and working directory
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app
# Copy project metadata and source code for installation
COPY pyproject.toml uv.lock ./
COPY scripts ./scripts/
# Install Python dependencies using uv (excluding dev dependencies)
RUN uv sync --no-dev
# Custom entrypoint to keep container running and signal readiness
ENTRYPOINT ["/bin/bash", "-c", "echo \"Container ready for Ollama setup and benchmarking.\" && tail -f /dev/null"]

73
LICENSE Normal file
View file

@ -0,0 +1,73 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright 2025 tom
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

185
README.md Normal file
View file

@ -0,0 +1,185 @@
# GPU LLM Benchmarking
Comprehensive benchmarking tool for testing Large Language Model inference performance across
different context window sizes, designed to evaluate GPU performance for informed upgrade decisions.
## Features
- **Dual Scenario Testing**: Tests both short prompts (allocation overhead) and half-context prompts
(realistic usage)
- **Comprehensive Metrics**: GPU utilization, memory bandwidth, VRAM usage, temperature, power
consumption, and generation speed
- **Configurable Context Windows**: Test from 8K to 128K contexts with geometric progression
- **Detailed Monitoring**: Real-time GPU metrics captured at 0.5s intervals
- **Statistical Analysis**: Mean and standard deviation calculations across multiple runs
- **Export Ready**: CSV outputs for community analysis and comparison
## Quick Start
### Using Docker (Recommended)
Pull and run the pre-configured Docker image with Ollama:
```bash
docker pull ghcr.io/tomfoster/gpu-llm-benchmark:latest
docker run --gpus all -v ./results:/app/results ghcr.io/tomfoster/gpu-llm-benchmark:latest
```
### Manual Installation
```bash
# Clone the repository
git clone https://git.tomfos.tr/tom/gpu-llm-benchmarking.git
cd gpu-llm-benchmarking
# Install dependencies
pip install requests
# Run quick validation
python scripts/llm_benchmark_python.py --quick
# Run full benchmark
python scripts/llm_benchmark_python.py
```
## Configuration
### Environment Variables
All configuration can be overridden using environment variables:
```bash
# Model configuration
export HF_MODEL="hf.co/unsloth/gemma-3-12b-it-GGUF:Q5_K_XL"
export TEST_ITERATIONS=3
# Context window configuration
export CONTEXT_START=8192
export CONTEXT_END=131072
export CONTEXT_MULTIPLIER=2
# Output directory
export OUTPUT_PATH="./benchmark_results"
# Ollama configuration
export OLLAMA_FLASH_ATTENTION=1
export OLLAMA_KEEP_ALIVE="10m"
export OLLAMA_KV_CACHE_TYPE="q8_0"
export OLLAMA_MAX_LOADED_MODELS=1
export OLLAMA_NUM_GPU=1
export OLLAMA_NUM_PARALLEL=1
```
### Docker Environment Variables
When using Docker, pass environment variables with `-e`:
```bash
docker run --gpus all \
-e HF_MODEL="your-model:tag" \
-e TEST_ITERATIONS=5 \
-e CONTEXT_END=65536 \
-v ./results:/app/results \
ghcr.io/tomfoster/gpu-llm-benchmark:latest
```
## Benchmark Modes
### Quick Mode
Tests only the starting context length (8K by default) for rapid validation:
```bash
python scripts/llm_benchmark_python.py --quick
# Or with Docker:
docker run --gpus all -v ./results:/app/results ghcr.io/tomfoster/gpu-llm-benchmark:latest --quick
```
### Full Mode
Complete benchmark suite testing all context lengths:
```bash
python scripts/llm_benchmark_python.py
# Or with Docker:
docker run --gpus all -v ./results:/app/results ghcr.io/tomfoster/gpu-llm-benchmark:latest
```
## Output Structure
Results are saved in timestamped directories:
```plain
benchmark_results_20250127_143022/
├── system_info.txt # GPU specs, CPU info, software versions
├── individual_results.csv # Raw test data
├── summary_stats.csv # Statistical analysis
├── benchmark.log # Execution log
├── prompt_*.txt # Test prompts
├── response_*.txt # Generated responses
├── ollama_response_*.json # Full API responses
└── gpu_monitor_*.csv # Real-time GPU metrics
```
## Metrics Collected
- **Performance**: Tokens/second, evaluation time, total duration
- **GPU Utilization**: Core usage percentage, memory bandwidth utilization
- **Memory**: VRAM used/free in GB
- **Thermal**: Temperature, power consumption, fan speed
- **Clocks**: Graphics and memory clock speeds
- **States**: GPU performance state (P0-P8)
## Repository Structure
- **[`scripts/`](scripts/)**: Benchmarking scripts and utilities
- `llm_benchmark_python.py`: Main benchmarking script
- Additional analysis tools (coming soon)
- **[`results/`](results/)**: Community benchmark results
- Organised by GPU model and configuration
- Includes raw data and analysis summaries
- [View all results](results/)
## Default Test Configuration
- **Model**: Gemma 3 12B Instruct (Q5_K_XL quantization)
- **Context Lengths**: 8K → 16K → 32K → 64K → 128K
- **Iterations**: 3 runs per context length
- **Scenarios**: Short prompts + Half-context prompts
## Requirements
### Hardware
- NVIDIA GPU with CUDA support
- Sufficient VRAM for target model and context lengths
- nvidia-smi installed and accessible
### Software
- Python 3.8+
- Docker (for containerized usage)
- Ollama (automatically installed if not present)
- NVIDIA drivers with nvidia-smi
## Community Contribution
Share your benchmark results to help the community:
1. Run the full benchmark suite
2. Compress the results directory
3. Share on the community forums with your GPU model and system specs
4. Compare results at [gpu-llm-benchmarks.com](https://gpu-llm-benchmarks.com)
## CI/CD
This repository includes GitHub Actions workflows that:
- Build and publish Docker images on each release
- Run validation tests on pull requests
- Ensure benchmark consistency across platforms
## License
Apache License 2.0 - See [LICENSE](LICENSE) file for details

215
docs/python-sdk-usage.md Normal file
View file

@ -0,0 +1,215 @@
---
title: Python SDK Usage
description: Documentation for the Vast.ai Python SDK, including installation, usage examples, and available methods.
source_url: https://docs.vast.ai/api/python-sdk-usage
---
We provide a [PyPI package](https://pypi.org/project/vastai/), `vastai-sdk`, for convenient Python usage.
## PyPI Install
You can install the latest stable PyPI release with:
```bash
pip install vastai-sdk
```
## Usage
Import the package:
```python
from vastai_sdk import VastAI
```
Construct a Vast client with your API key:
```python
vast_sdk = VastAI(api_key='YOUR_API_KEY')
```
## Resource Methods
Most useful available VastAI resources are implemented. Commands for Vast CLI usage should have
equivalent class methods on your Vast client object. Most IDEs should show type hints and relevant
class methods and their expected arguments for VastAI, due to the implementation of our base class.
For example, the CLI command `vastai show instances` has the equivalent,`vast_sdk.show_instances()`.
```python
from vastai_sdk import VastAI
vast_sdk = VastAI(api_key='YOUR_API_KEY')
output = vast_sdk.show_instances()
print(output)
```
## Example Usage
Here are some example usages of our Python SDK class `VastAI`:
### Starting and Stopping Instances
```python
from vastai_sdk import VastAI
vast_sdk = VastAI(api_key='YOUR_API_KEY')
vast_sdk.start_instance(ID=12345678)
vast_sdk.stop_instance(ID=12345678)
```
### Creating a New Instance
Create a new instance based on given parameters (performs search offers + create instance).
```python
from vastai_sdk import VastAI
vast_sdk = VastAI(api_key='YOUR_API_KEY')
vast_sdk.launch_instance(num_gpus="1", gpu_name="RTX_3090", image="pytorch/pytorch")
```
### Copying Files Between Instances
```python
from vastai_sdk import VastAI
vast_sdk = VastAI(api_key='YOUR_API_KEY')
vast_sdk.copy(src='source_path', dst='destination_path', identity='identity_file')
```
### Managing SSH Keys
Create a new SSH key, show all SSH keys, and delete an SSH key.
```python
from vastai_sdk import VastAI
vast_sdk = VastAI(api_key='YOUR_API_KEY')
vast_sdk.create_ssh_key(ssh_key='your_ssh_key')
ssh_keys = vast_sdk.show_ssh_keys()
print(ssh_keys)
vast_sdk.delete_ssh_key(ID=123456)
```
### Contribution and Issue Reporting
This [code repository](https://github.com/vast-ai/vast-python) is open source and can be rapidly
changing at times. If you find a potential bug, please open an issue on GitHub. If you wish to
contribute to improving this code and its functionality, feel welcome to open a PR with any
improvements on our [GitHub repository](https://github.com/vast-ai/vast-python).
### Available Methods
Below is a list of the available methods you can call on the `VastAI` client. These methods are
categorised for better readability.
#### Instance Management
| Method | Description |
|---|---|
| `start_instance(ID: int)` | Start an instance. |
| `stop_instance(ID: int)` | Stop an instance. |
| `reboot_instance(ID: int)` | Reboot an instance. |
| `destroy_instance(id: int)` | Destroy an instance. |
| `destroy_instances(ids: List[int])` | Destroy multiple instances. |
| `recycle_instance(ID: int)` | Recycle an instance. |
| `label_instance(id: int, label: str)` | Label an instance. |
| `show_instance(id: int)` | Show details of an instance. |
| `show_instances(quiet: bool = False)` | Show all instances. |
| `logs(INSTANCE_ID: int, tail: Optional[str] = None)` | Retrieve logs for an instance. |
| `execute(ID: int, COMMAND: str)` | Execute a command on an instance. |
| `launch_instance(...)` | Launch a new instance with various parameters. |
#### SSH Key Management
| Method | Description |
|---|---|
| `create_ssh_key(ssh_key: str)` | Create a new SSH key. |
| `delete_ssh_key(ID: int)` | Delete an SSH key. |
| `show_ssh_keys()` | Show all SSH keys. |
| `attach_ssh(instance_id: int, ssh_key: str)` | Attach an SSH key to an instance. |
| `detach_ssh(instance_id: int, ssh_key_id: str)` | Detach an SSH key from an instance. |
#### API Key Management
| Method | Description |
|---|---|
| `create_api_key(name: Optional[str] = None, ...)` | Create a new API key. |
| `delete_api_key(ID: int)` | Delete an API key. |
| `reset_api_key()` | Reset the API key. |
| `show_api_key(id: int)` | Show details of an API key. |
| `show_api_keys()` | Show all API keys. |
| `set_api_key(new_api_key: str)` | Set a new API key. |
#### Autoscaler Management
| Method | Description |
|---|---|
| `create_autoscaler(test_workers: int = 3, ...)` | Create a new autoscaler. |
| `update_autoscaler(ID: int, min_load: Optional[float] = None, ...)` | Update an autoscaler. |
| `delete_autoscaler(ID: int)` | Delete an autoscaler. |
| `show_autoscalers()` | Show all autoscalers. |
#### Endpoint Management
| Method | Description |
|---|---|
| `create_endpoint(min_load: float = 0.0, ...)` | Create a new endpoint. |
| `update_endpoint(ID: int, min_load: Optional[float] = None, ...)` | Update an endpoint. |
| `delete_endpoint(ID: int)` | Delete an endpoint. |
| `show_endpoints()` | Show all endpoints. |
#### File Management
| Method | Description |
|---|---|
| `copy(src: str, dst: str, identity: Optional[str] = None)` | Copy files between instances. |
| `cloud_copy(src: Optional[str] = None, dst: Optional[str] = "/workspace", ...)` | Copy files between cloud and instance. |
| `cancel_copy(dst: str)` | Cancel a file copy operation. |
| `cancel_sync(dst: str)` | Cancel a file sync operation. |
| `scp_url(id: int)` | Get the SCP URL for transferring files to/from an instance. |
#### Team Management
| Method | Description |
|---|---|
| `create_team(team_name: Optional[str] = None)` | Create a new team. |
| `destroy_team()` | Destroy a team. |
| `invite_team_member(email: Optional[str] = None, role: Optional[str] = None)` | Invite a new member to the team. |
| `remove_team_member(ID: int)` | Remove a member from the team. |
| `create_team_role(name: Optional[str] = None, permissions: Optional[str] = None)` | Create a new team role. |
| `remove_team_role(NAME: str)` | Remove a role from the team. |
| `update_team_role(ID: int, name: Optional[str] = None, permissions: Optional[str] = None)` | Update details of a team role. |
| `show_team_members()` | Show all team members. |
| `show_team_role(NAME: str)` | Show details of a specific team role. |
| `show_team_roles()` | Show all team roles. |
#### Host Management
| Method | Description |
|---|---|
| `cleanup_machine(ID: int)` | Clean up a machine's configuration and resources. |
| `list_machine(ID: int, price_gpu: Optional[float] = None, price_disk: Optional[float] = None, price_inetu: Optional[float] = None, price_inetd: Optional[float] = None, discount_rate: Optional[float] = None, min_chunk: Optional[int] = None, end_date: Optional[str] = None)` | List details of a single machine with optional pricing and configuration parameters. |
| `list_machines(IDs: List[int], price_gpu: Optional[float] = None, price_disk: Optional[float] = None, price_inetu: Optional[float] = None, price_inetd: Optional[float] = None, discount_rate: Optional[float] = None, min_chunk: Optional[int] = None, end_date: Optional[str] = None)` | List details of multiple machines with optional pricing and configuration parameters. |
| `remove_defjob(id: int)` | Remove the default job from a machine. |
| `set_defjob(id: int, price_gpu: Optional[float] = None, price_inetu: Optional[float] = None, price_inetd: Optional[float] = None, image: Optional[str] = None, args: Optional[List[str]] = None)` | Set a default job on a machine with specified parameters. |
| `set_min_bid(id: int, price: Optional[float] = None)` | Set the minimum bid price for a machine. |
| `schedule_maint(id: int, sdate: Optional[float] = None, duration: Optional[float] = None)` | Schedule maintenance for a machine. |
| `cancel_maint(id: int)` | Cancel scheduled maintenance for a machine. |
| `unlist_machine(id: int)` | Unlist a machine from being available for new jobs. |
| `show_machines(quiet: bool = False, filter: Optional[str] = None)` | Retrieve and display a list of machines based on specified criteria. |
#### Other Methods
| Method | Description |
|---|---|
| `get_gpu_names()` | Returns a set of GPU names available on Vast.ai. |
| `show_connections()` | Show all connections. |
| `show_deposit(ID: int)` | Show deposit details for an instance. |
| `show_earnings(quiet: bool = False, start_date: Optional[str] = None, end_date: Optional[str] = None, machine_id: Optional[int] = None)` | Show earnings information. |
| `show_invoices(quiet: bool = False, start_date: Optional[str] = None, end_date: Optional[str] = None, ...)` | Show invoice details. |
| `show_ipaddrs()` | Show IP addresses. |
| `show_user(quiet: bool = False)` | Show user details. |
| `show_subaccounts(quiet: bool = False)` | Show all subaccounts of the current user. |
| `transfer_credit(recipient: str, amount: float)` | Transfer credit to another account. |
| `update_ssh_key(id: int, ssh_key: str)` | Update an SSH key. |
| `generate_pdf_invoices(quiet: bool = False, start_date: Optional[str] = None, end_date: Optional[str] = None, only_charges: bool = False, only_credits: bool = False)` | Generate PDF invoices based on filters. |
Please refer to the VastAI Python SDK [API Reference](https://github.com/vast-ai/vast-python) for
detailed information on all available methods and their usage.

111
pyproject.toml Normal file
View file

@ -0,0 +1,111 @@
[project]
name = "gpu-llm-benchmarking"
version = "0.1.0"
description = "Benchmarking tool for testing LLM inference performance across different context window sizes on various GPUs."
readme = "README.md"
license = { text = "Apache-2.0" }
authors = [{ name = "Tom Foster", email = "tom@tomfos.tr" }]
maintainers = [{ name = "Tom Foster", email = "tom@tomfos.tr" }]
requires-python = ">=3.11"
classifiers = [
"Development Status :: 4 - Beta",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: System :: Benchmark",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"paramiko>=3",
"python-dotenv>=1",
"requests>=2",
"vastai-sdk>=0",
]
[project.urls]
Homepage = "https://git.tomfos.tr/tom/gpu-llm-benchmarking"
"Bug Reports" = "https://git.tomfos.tr/tom/gpu-llm-benchmarking/issues"
"Source" = "https://git.tomfos.tr/tom/gpu-llm-benchmarking"
[dependency-groups]
dev = ["ruff>=0", "uv>=0", "mypy>=1", "types-requests>=2", "types-paramiko>=3"]
[tool.uv]
package = true
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project.scripts]
benchmark-llm = "scripts.llm_benchmark:main"
[tool.setuptools]
packages = { find = {} }
[tool.ruff]
cache-dir = "/tmp/.ruff_cache"
fix = true
line-length = 100
preview = true
show-fixes = false
target-version = "py311"
unsafe-fixes = true
[tool.ruff.format]
line-ending = "auto"
skip-magic-trailing-comma = false
[tool.ruff.lint]
fixable = ["ALL"]
ignore = [
"ANN401", # use of Any type
"BLE001", # blind Exception usage
"COM812", # missing trailing comma
"CPY", # flake8-copyright
"FBT", # boolean arguments
"PLR0912", # too many branches
"PLR0913", # too many arguments
"PLR0915", # too many statements
"PLR0917", # too many positional arguments
"PLR6301", # method could be static
"RUF029", # async methods that don't await
"S104", # binding to all interfaces
"S110", # passed exceptions
"S404", # subprocess module usage (needed for nvidia-smi)
"S602", # subprocess with shell=True
"S603", # subprocess.run(check=False)
"S607", # starting process with partial path
"TRY301", # raise inside try block
]
select = ["ALL"]
unfixable = [
"F841", # local variable assigned but never used
"RUF100", # unused noqa comments
"T201", # don't strip print statement
]
[tool.ruff.lint.isort]
combine-as-imports = true
required-imports = ["from __future__ import annotations"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = true
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
follow_imports = "normal"
ignore_missing_imports = true

77
results/README.md Normal file
View file

@ -0,0 +1,77 @@
# GPU LLM Benchmark Results
This directory contains comprehensive benchmark results from LLM context length tests across various
GPU configurations. Results are organised by GPU type, model configuration, and test parameters
following a standardised naming convention.
## Directory Structure and Naming Convention
Results are automatically organised into `{GPU_TYPE}_{NUM_GPUS}x_Ollama-{VERSION}_{OLLAMA_MODEL}`
(where all non-dot/hyphen special characters inside fields are replaced with a hyphen to standardise
names):
```plain
results/
├── README.md # This file - overview and comparisons
├── RTX-4090_1x_Ollama-0.9.6_hf.co-unsloth-gemma-3-12b-it-GGUF-Q5_K_XL/
│ ├── README.md # Summary for this specific configuration
│ ├── individual_results.csv # Raw test data
│ ├── summary_stats.csv # Statistical analysis
│ ├── system_info.txt # Hardware and software details
│ └── benchmark.log # Complete execution log
├── RTX-5090_1x_Ollama-0.9.6_hf.co-unsloth-gemma-3-12b-it-GGUF-Q5-K-XL/
├── RTX-5090_2x_Ollama-0.9.6_hf.co-unsloth-gemma-3-12b-it-GGUF-Q5-K-XL/
```
## Result Documentation
### Individual Result Directories
Each result directory contains a complete benchmark dataset and will include:
- **README.md**: Detailed analysis of that specific test run including:
- Performance highlights and key findings
- Interesting outliers or anomalies observed
- Context length scaling characteristics
- GPU utilisation patterns and efficiency metrics
- Comparison with expected theoretical performance
- **Raw Data Files**:
- `individual_results.csv`: Every test iteration with full metrics
- `summary_stats.csv`: Statistical summaries grouped by scenario and context length
- `system_info.txt`: Complete hardware/software configuration for reproducibility
- `benchmark.log`: Detailed execution logs for debugging
### This README (Comparative Analysis)
This README serves as the central hub for cross-configuration comparisons and analysis, including:
- Performance trends across different GPU generations
- Scaling efficiency with multi-GPU configurations
- Model-specific optimisation insights
- Power efficiency and cost-effectiveness analysis
- Recommendations for different use cases
## Testing Methodology and Data Collection
Our benchmark suite tests LLM inference performance across different context window sizes using two
scenarios: short prompts (allocation overhead testing) and half-context prompts (realistic usage
testing). Each test captures comprehensive GPU metrics including utilisation, memory bandwidth, VRAM
usage, temperature, power consumption, and clock speeds at 0.5-second intervals.
Results include individual test iterations with full metrics, statistical summaries grouped by
scenario and context length, complete hardware/software configurations for reproducibility, and
detailed execution logs. This approach provides insight into GPU performance characteristics,
context length scaling behaviour, and resource utilisation patterns across different hardware
configurations.
## Contributing Results
This repository primarily documents my own benchmark test results across various GPU configurations.
If you'd like to see results for a specific GPU configuration, please open an
[issue](https://git.tomfos.tr/tom/gpu-llm-benchmarking/issues) requesting it and I'll consider
adding it to my testing queue.
Alternatively, if you fork this repository and would like to contribute results back via pull
request, I may include them if they add significant value to the dataset.

58
scripts/README.md Normal file
View file

@ -0,0 +1,58 @@
# Scripts
This directory contains all benchmarking and utility scripts for the GPU LLM Benchmarking project.
1. [Available Scripts](#available-scripts)
2. [Running Locally](#running-locally)
3. [Running on Vast.ai](#running-on-vastai)
## Available Scripts
| Script | About |
|---|---|
| llm_benchmark.py | The main benchmarking script that tests LLM inference performance across different context window
sizes using dual scenario testing (short prompts vs half-context), comprehensive GPU monitoring, and
statistical analysis across multiple runs. |
| run_vast.ai_benchmark.py | Remote benchmarking runner for executing LLM benchmarks on Vast.ai GPU instances. Handles the
complete lifecycle including instance provisioning, Ollama installation and configuration, benchmark
execution, results retrieval, and resource cleanup. |
## Running Locally
For testing on your own hardware:
```bash
# Install uv if missing
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or update your existing copy
uv self update
# Path is important to load the .env from the directory above
uv run scripts/llm_benchmark.py
```
Copy the [.env.example](../.env.example) to `.env` locally, and configure it as required. This
script does not install [Ollama](https://github.com/ollama/ollama#ollama) - it expects
[Ollama](https://github.com/ollama/ollama#ollama) to already be running with your desired model
[pre-pulled](https://github.com/ollama/ollama#pull-a-model).
## Running on Vast.ai
For testing across different GPU configurations:
```bash
# Install uv if missing
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or update your existing copy
uv self update
# Path is important to load the .env from the directory above
uv run scripts/run_vast.ai_benchmark.py
```
Copy the [.env.example](../.env.example) to `.env` locally, and configure it as required, including
an [API key from Vast.ai](https://cloud.vast.ai/manage-keys/). The script automatically provisions
the cheapest instance meeting the parameters, installs dependencies, executes benchmarks, then
retrieves the results.

View file

@ -0,0 +1,7 @@
"""Helper modules for GPU LLM benchmarking orchestration.
This package contains modular components for the benchmarking system,
organised by responsibility to follow SOLID principles.
"""
from __future__ import annotations

View file

@ -0,0 +1,271 @@
"""Interactive SSH session management for remote command execution.
This module provides functionality for executing commands on remote instances
using pexpect for real-time output streaming and interactive session management.
"""
from __future__ import annotations
import logging
import time
from typing import TYPE_CHECKING
import paramiko
if TYPE_CHECKING:
from paramiko import ChannelFile
logger = logging.getLogger(__name__)
class SSHInteractiveSession:
"""Manages SSH sessions for remote command execution and file transfer."""
def __init__(self, ssh_host: str, ssh_port: int, ssh_user: str = "root") -> None:
"""Initialise SSH session parameters."""
self.ssh_host = ssh_host
self.ssh_port = ssh_port
self.ssh_user = ssh_user
self.client: paramiko.SSHClient | None = None
self.sftp: paramiko.SFTPClient | None = None
def _clean_output(self, text: str) -> str:
"""Returns the text as-is without any filtering.
Returns:
The unmodified text.
"""
return text
def connect(self, timeout: int = 30) -> None:
"""Establish SSH connection using Paramiko.
Raises:
RuntimeError: If the connection fails.
"""
if self.client:
transport = self.client.get_transport()
if transport and transport.is_active():
logger.debug("SSH session already connected.")
return
logger.debug(
"Establishing SSH connection to %s@%s:%s", self.ssh_user, self.ssh_host, self.ssh_port
)
try:
self.client = paramiko.SSHClient()
self.client.load_system_host_keys()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.client.connect(
hostname=self.ssh_host,
port=self.ssh_port,
username=self.ssh_user,
timeout=timeout,
auth_timeout=timeout,
banner_timeout=timeout,
)
# Enable keepalive to detect broken connections
transport = self.client.get_transport()
if transport:
transport.set_keepalive(30) # Send keepalive every 30 seconds
self.sftp = self.client.open_sftp()
logger.debug("SSH connection established.")
except paramiko.AuthenticationException as e:
msg = f"Authentication failed: {e}"
raise RuntimeError(msg) from e
except paramiko.SSHException as e:
msg = f"SSH connection failed: {e}"
raise RuntimeError(msg) from e
except Exception as e:
msg = f"Failed to connect: {e}"
raise RuntimeError(msg) from e
def execute_command_interactive(self, command: str, timeout: int = 600) -> None:
"""Execute a command with real-time output.
Raises:
RuntimeError: If the command fails.
"""
if not self.client:
self.connect()
if not self.client:
msg = "SSH session is not properly initialised."
raise RuntimeError(msg)
# Check if connection is still alive
transport = self.client.get_transport()
if not transport or not transport.is_active():
logger.warning("SSH connection lost, reconnecting...")
self.close()
self.connect()
logger.debug("Executing command: %s", command)
_stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
output = stdout.read().decode("utf-8")
error_output = stderr.read().decode("utf-8")
exit_status = stdout.channel.recv_exit_status()
# Debug log sanitised SSH response
sanitised_output = self._clean_output(output)
sanitised_error = self._clean_output(error_output)
logger.debug(
"SSH Response [exit=%d]: stdout=%r stderr=%r",
exit_status,
sanitised_output[:500],
sanitised_error[:500],
)
if exit_status != 0:
error_msg = f"Command '{command}' failed with exit code {exit_status}."
logger.error(error_msg)
logger.error("Output:\n%s", output)
logger.error("Error Output:\n%s", error_output)
raise RuntimeError(error_msg)
# For interactive commands, we don't return output, just ensure it ran successfully.
# The original _execute_command would have logged it.
def execute_command_capture(self, command: str, timeout: int = 30) -> str:
"""Execute a command and capture its output.
Returns:
The command output.
Raises:
RuntimeError: If the SSH session is not established or command fails.
"""
if not self.client:
self.connect()
if not self.client:
msg = "SSH session is not properly initialised."
raise RuntimeError(msg)
# Check if connection is still alive
transport = self.client.get_transport()
if not transport or not transport.is_active():
logger.warning("SSH connection lost, reconnecting...")
self.close()
self.connect()
logger.debug("Capturing output for command: %s", command)
_stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
output = stdout.read().decode("utf-8")
error_output = stderr.read().decode("utf-8")
exit_status = stdout.channel.recv_exit_status()
# Debug log sanitised SSH response
sanitised_output = self._clean_output(output)
sanitised_error = self._clean_output(error_output)
logger.debug(
"SSH Response [exit=%d]: stdout=%r stderr=%r",
exit_status,
sanitised_output[:500],
sanitised_error[:500],
)
if exit_status != 0:
error_msg = f"Command '{command}' failed with exit code {exit_status}."
logger.error(error_msg)
logger.error("Output:\n%s", output)
logger.error("Error Output:\n%s", error_output)
raise RuntimeError(error_msg)
return output
def execute_command_background(self, command: str) -> None:
"""Execute a command in the background without waiting for output.
This method is designed for fire-and-forget commands that run in the
background (e.g., using nohup, &, or disown). It returns immediately
after launching the command without waiting for completion or output.
Raises:
RuntimeError: If the SSH session is not established.
"""
if not self.client:
self.connect()
if not self.client:
msg = "SSH session is not properly initialised."
raise RuntimeError(msg)
# Check if connection is still alive
transport = self.client.get_transport()
if not transport or not transport.is_active():
logger.warning("SSH connection lost, reconnecting...")
self.close()
self.connect()
logger.debug("Executing background command: %s", command)
# Execute command without waiting for output
self.client.exec_command(command)
# Return immediately without reading stdout/stderr or waiting for exit status
def _stream_output_chunk(self, stdout: ChannelFile, stderr: ChannelFile) -> None:
"""Process and log a chunk of streaming output."""
if stdout.channel.recv_ready():
chunk = stdout.read(1024).decode("utf-8", errors="replace")
if chunk:
# Log each chunk for real-time progress
cleaned_chunk = self._clean_output(chunk)
logger.info(cleaned_chunk.rstrip())
if stderr.channel.recv_ready():
chunk = stderr.read(1024).decode("utf-8", errors="replace")
if chunk:
cleaned_chunk = self._clean_output(chunk)
logger.warning(cleaned_chunk.rstrip())
def execute_command_streaming(self, command: str, timeout: int = 600) -> None:
"""Execute a command with real-time output streaming.
This method provides real-time output streaming for long-running commands
like model downloads, showing progress as it happens.
Raises:
RuntimeError: If the SSH session is not established or command fails.
"""
if not self.client:
self.connect()
if not self.client:
msg = "SSH session is not properly initialised."
raise RuntimeError(msg)
# Check if connection is still alive
transport = self.client.get_transport()
if not transport or not transport.is_active():
logger.warning("SSH connection lost, reconnecting...")
self.close()
self.connect()
logger.debug("Executing streaming command: %s", command)
_stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
# Stream output in real-time
while not stdout.channel.exit_status_ready():
self._stream_output_chunk(stdout, stderr)
# Small delay to prevent excessive polling
time.sleep(0.1)
# Get final exit status
exit_status = stdout.channel.recv_exit_status()
# Get any remaining output
final_stdout = stdout.read().decode("utf-8", errors="replace")
final_stderr = stderr.read().decode("utf-8", errors="replace")
if final_stdout:
logger.info(self._clean_output(final_stdout).rstrip())
if final_stderr:
logger.warning(self._clean_output(final_stderr).rstrip())
if exit_status != 0:
error_msg = f"Command '{command}' failed with exit code {exit_status}."
raise RuntimeError(error_msg)
def close(self) -> None:
"""Close the SSH and SFTP sessions."""
if self.sftp:
self.sftp.close()
self.sftp = None
if self.client:
self.client.close()
self.client = None

79
scripts/helpers/logger.py Normal file
View file

@ -0,0 +1,79 @@
"""Coloured logging configuration for the benchmarking system.
This module provides a pre-configured logger with coloured output formatting
for improved readability during benchmark execution and debugging.
"""
from __future__ import annotations
import logging
import re
from typing import ClassVar
class LogMessageFilter(logging.Filter):
"""A logging filter to remove problematic control characters from log messages."""
def filter(self, record: logging.LogRecord) -> bool:
"""Filter log records, removing only problematic control characters.
Returns:
True if the log record should be processed, False otherwise.
"""
if isinstance(record.msg, str):
# Remove only problematic control characters, preserve emoji and printable Unicode
record.msg = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", record.msg)
if record.exc_text:
record.exc_text = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", record.exc_text)
return True
class ColoredFormatter(logging.Formatter):
"""Custom formatter that adds colour codes to different log levels."""
# ANSI colour codes
COLORS: ClassVar[dict[str, str]] = {
"DEBUG": "\033[90m", # Grey
"INFO": "\033[92m", # Green
"WARNING": "\033[93m", # Yellow
"ERROR": "\033[91m", # Red
"CRITICAL": "\033[95m", # Magenta
}
RESET: ClassVar[str] = "\033[0m"
def format(self, record: logging.LogRecord) -> str:
"""Format the log record with appropriate colours.
Returns:
The formatted log record with appropriate colours.
"""
# Get the colour for this log level
colour = self.COLORS.get(record.levelname, "")
# Format the message
formatted = super().format(record)
# Apply colour to the entire line
if colour:
formatted = f"{colour}{formatted}{self.RESET}"
return formatted
# Create and configure logger with colour formatting
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Create console handler with colour formatter
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(ColoredFormatter("%(asctime)s - %(levelname)s - %(message)s"))
# Add the custom filter to the console handler
console_handler.addFilter(LogMessageFilter())
# Add handler to logger
logger.addHandler(console_handler)
# Prevent duplicate logs from root logger
logger.propagate = False

245
scripts/helpers/models.py Normal file
View file

@ -0,0 +1,245 @@
"""Data models and configuration classes for GPU LLM benchmarking.
This module contains all dataclasses and configuration models used across
the benchmarking system, providing a single source of truth for data structures.
"""
from __future__ import annotations
import os
import re
from dataclasses import dataclass
from pathlib import Path
from dotenv import dotenv_values
@dataclass
class RemoteBenchmarkConfig:
"""Configuration settings for remote benchmark execution.
Encapsulates all environment variables and settings required for
orchestrating remote GPU benchmarking workflows. All values must
be provided via environment variables or .env file.
"""
# Vast.ai configuration
api_key: str
gpu_type: str
num_gpus: int
disk_space: int
# Benchmark parameters
test_iterations: str
context_start: str
context_end: str
context_multiplier: str
# Ollama configuration
ollama_model: str
ollama_flash_attention: str
ollama_keep_alive: str
ollama_kv_cache_type: str
ollama_max_loaded_models: str
ollama_num_gpu: str
ollama_num_parallel: str
# Local storage
local_results_dir: str
@classmethod
def from_dotenv(cls, env_file: str = ".env") -> RemoteBenchmarkConfig:
"""Create configuration from .env file.
Returns:
RemoteBenchmarkConfig: An instance of RemoteBenchmarkConfig populated with values
from the .env file.
"""
config = dotenv_values(env_file)
def get_required(key: str) -> str:
value = config.get(key)
if value is None:
msg = f"Missing required environment variable: {key}"
raise ValueError(msg)
return value
def get_required_int(key: str) -> int:
value = get_required(key)
try:
return int(value)
except ValueError as e:
msg = f"Invalid integer value for environment variable {key}: {value}"
raise ValueError(msg) from e
return cls(
api_key=get_required("VAST_API_KEY"),
gpu_type=get_required("GPU_TYPE"),
num_gpus=get_required_int("NUM_GPUS"),
disk_space=get_required_int("DISK_SPACE"),
test_iterations=get_required("TEST_ITERATIONS"),
context_start=get_required("CONTEXT_START"),
context_end=get_required("CONTEXT_END"),
context_multiplier=get_required("CONTEXT_MULTIPLIER"),
ollama_model=get_required("OLLAMA_MODEL"),
ollama_flash_attention=get_required("OLLAMA_FLASH_ATTENTION"),
ollama_keep_alive=get_required("OLLAMA_KEEP_ALIVE"),
ollama_kv_cache_type=get_required("OLLAMA_KV_CACHE_TYPE"),
ollama_max_loaded_models=get_required("OLLAMA_MAX_LOADED_MODELS"),
ollama_num_gpu=get_required("OLLAMA_NUM_GPU"),
ollama_num_parallel=get_required("OLLAMA_NUM_PARALLEL"),
local_results_dir=get_required("LOCAL_RESULTS_DIR"),
)
@property
def results_folder_name(self) -> str:
"""Generate standardised results folder name based on configuration.
Format: {GPU_TYPE}_{NUM_GPUS}x_Ollama-{VERSION}_{OLLAMA_MODEL}
All non-dot/hyphen special characters are replaced with hyphens.
"""
# Sanitise GPU type (replace special chars except dots/hyphens with hyphens)
gpu_clean = self._sanitise_name_component(self.gpu_type)
# Sanitise model name
model_clean = self._sanitise_name_component(self.ollama_model)
# Get Ollama version - will be set by the orchestrator
ollama_version = getattr(self, "_ollama_version", "unknown")
return f"{gpu_clean}_{self.num_gpus}x_Ollama-{ollama_version}_{model_clean}"
def _sanitise_name_component(self, name: str) -> str:
"""Sanitise name component by replacing special chars (except dots/hyphens) with hyphens.
Returns:
Sanitised name with special characters replaced by hyphens.
"""
# Replace any character that's not alphanumeric, dot, or hyphen with hyphen
return re.sub(r"[^a-zA-Z0-9.-]", "-", name)
def set_ollama_version(self, version: str) -> None:
"""Set the Ollama version for results folder naming."""
self._ollama_version = version
@property
def remote_results_path(self) -> str:
"""Generate remote results path for benchmark output."""
return f"/app/results/{self.results_folder_name}"
@property
def local_results_path(self) -> Path:
"""Generate local results path for downloaded files."""
return Path(self.local_results_dir) / self.results_folder_name
# Global configuration defaults from environment variables
HF_MODEL = os.getenv("OLLAMA_MODEL", "hf.co/unsloth/gemma-3-12b-it-GGUF:Q5_K_XL")
TEST_ITERATIONS = int(os.getenv("TEST_ITERATIONS", "3"))
CONTEXT_START = int(os.getenv("CONTEXT_START", "8192"))
CONTEXT_END = int(os.getenv("CONTEXT_END", "131072"))
CONTEXT_MULTIPLIER = float(os.getenv("CONTEXT_MULTIPLIER", "2"))
@dataclass
class LocalBenchmarkConfig:
"""Configuration settings for local benchmark execution.
Handles context length generation based on start/end/multiplier parameters
and scenario configuration for local GPU benchmarking.
"""
model: str = HF_MODEL
context_lengths: list[int] | None = None
runs_per_context: int = TEST_ITERATIONS
scenarios: list[str] | None = None
context_start: int = CONTEXT_START
context_end: int = CONTEXT_END
context_multiplier: float = CONTEXT_MULTIPLIER
short_prompt: str = (
"Write a detailed technical explanation of how neural networks work, "
"including backpropagation, gradient descent, and modern architectures "
"like transformers. Include examples and mathematical concepts."
)
filler_text: str = (
"Neural networks represent a fundamental paradigm in artificial intelligence, "
"drawing inspiration from biological neural systems to create computational "
"models capable of learning complex patterns from data. These interconnected "
"networks of artificial neurons have revolutionised machine learning and "
"enabled breakthroughs in computer vision, natural language processing, "
"and many other domains."
)
def __post_init__(self) -> None:
"""Initialise computed fields after object creation."""
if self.context_lengths is None:
self.context_lengths = self._generate_context_lengths()
if self.scenarios is None:
self.scenarios = ["short", "half_context"]
def _generate_context_lengths(self) -> list[int]:
"""Generate context lengths based on start, end, and multiplier.
Returns:
List of context lengths following geometric progression.
"""
lengths = []
current = self.context_start
while current <= self.context_end:
lengths.append(current)
next_length = int(current * self.context_multiplier)
if next_length > self.context_end:
break
current = next_length
return lengths
@dataclass
class TestResult:
"""Single benchmark test result with comprehensive GPU and performance metrics.
Stores all data from one test iteration including Ollama response metrics,
GPU utilization statistics, and timing information.
"""
timestamp: str
scenario: str
context_length: int
run_number: int
tokens_per_second: float
total_time_seconds: float
prompt_eval_count: int
prompt_eval_duration_ms: float
eval_count: int
eval_duration_ms: float
total_duration_ms: float
load_duration_ms: float
response_length_chars: int
gpu_util_avg: float
gpu_util_max: float
gpu_util_min: float
gpu_mem_util_avg: float
gpu_mem_util_max: float
gpu_mem_util_min: float
vram_used_avg_mb: float
vram_used_max_mb: float
vram_used_min_mb: float
vram_free_avg_mb: float
vram_free_max_mb: float
vram_free_min_mb: float
gpu_temp_avg: float
gpu_temp_max: float
gpu_temp_min: float
gpu_power_avg: float
gpu_power_max: float
gpu_power_min: float
gpu_clock_graphics_avg: float
gpu_clock_memory_avg: float
gpu_fan_speed_avg: float
gpu_pstate_mode: str
test_id: str

View file

@ -0,0 +1,201 @@
"""NVIDIA GPU monitoring for benchmarking.
This module provides the GPUMonitor class for comprehensive GPU metrics collection
during benchmark execution, including utilization, memory, temperature, and power data.
"""
from __future__ import annotations
import csv
import subprocess
import time
from contextlib import contextmanager
from pathlib import Path
from statistics import mean
from threading import Event, Thread
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Generator
# GPU monitoring constants
GPU_QUERY_FIELDS = (
"utilization.gpu,utilization.memory,memory.used,memory.free,"
"temperature.gpu,power.draw,clocks.current.graphics,"
"clocks.current.memory,fan.speed,pstate"
)
GPU_QUERY_FIELD_COUNT = 10
PSTATE_INDEX = 9
GPU_METRIC_COUNT = 8
GPU_CSV_HEADERS = [
"timestamp",
"gpu_util",
"gpu_mem_util",
"vram_used_mb",
"vram_free_mb",
"temperature_c",
"power_watts",
"clock_graphics_mhz",
"clock_memory_mhz",
"fan_speed_percent",
"pstate",
]
class GPUMonitor:
"""Monitors comprehensive GPU metrics during benchmark execution.
Captures GPU utilization, memory bandwidth, VRAM usage, temperature,
power consumption, clock speeds, and performance states at 0.5s intervals.
"""
def __init__(self, output_file: Path) -> None:
"""Initialise GPU monitor with output file for metrics storage.
Args:
output_file: Path to store GPU monitoring metrics
"""
self.output_file = output_file
self.metrics: list[tuple] = []
self.stop_event = Event()
self.thread: Thread | None = None
def _get_gpu_stats(self) -> tuple:
"""Get comprehensive GPU metrics via nvidia-smi.
Returns:
Tuple of (gpu_util, mem_util, vram_used, vram_free, temp, power,
clock_graphics, clock_memory, fan_speed, pstate).
"""
try:
result = subprocess.run(
[
"/usr/bin/nvidia-smi",
f"--query-gpu={GPU_QUERY_FIELDS}",
"--format=csv,noheader,nounits",
],
check=False,
capture_output=True,
text=True,
timeout=5,
)
if result.returncode != 0:
return self._default_gpu_stats()
values = [x.strip() for x in result.stdout.strip().split(",")]
if len(values) < GPU_QUERY_FIELD_COUNT:
return self._default_gpu_stats()
return self._parse_gpu_values(values)
except (subprocess.TimeoutExpired, ValueError, IndexError):
return self._default_gpu_stats()
def _default_gpu_stats(self) -> tuple:
"""Return default GPU stats when nvidia-smi is unavailable."""
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, "P0")
def _parse_gpu_values(self, values: list[str]) -> tuple:
"""Parse nvidia-smi output values into appropriate types.
Returns:
Tuple of parsed values.
"""
parsed_values = []
for i, val in enumerate(values):
if val in {"N/A", "[N/A]"}:
parsed_values.append(0.0 if i < GPU_METRIC_COUNT else "P0") # Default for pstate
elif i == PSTATE_INDEX: # pstate is string
parsed_values.append(val)
else:
try:
parsed_values.append(float(val))
except ValueError:
parsed_values.append(0.0)
return tuple(parsed_values)
def _monitor_loop(self) -> None:
"""Continuous monitoring loop that runs in background thread.
Captures GPU metrics every 0.5 seconds and writes to CSV file.
"""
with Path(self.output_file).open("w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(GPU_CSV_HEADERS)
while not self.stop_event.is_set():
timestamp = time.time()
stats = self._get_gpu_stats()
self.metrics.append((timestamp, *stats))
writer.writerow([timestamp, *list(stats)])
f.flush()
time.sleep(0.5)
@contextmanager
def monitor(self) -> Generator[None, None, None]:
"""Context manager for GPU monitoring during test execution.
Yields:
None while monitoring is active.
"""
self.metrics.clear()
self.stop_event.clear()
self.thread = Thread(target=self._monitor_loop, daemon=True)
self.thread.start()
try:
yield
finally:
self.stop_event.set()
if self.thread:
self.thread.join(timeout=2)
def get_stats(self) -> tuple:
"""Calculate comprehensive statistics from collected metrics.
Returns:
Tuple of min/avg/max values for all monitored metrics and most common pstate.
"""
if not self.metrics:
return (0.0,) * 16 + ("P0",)
# Extract each metric type (skip timestamp)
gpu_utils = [m[1] for m in self.metrics]
gpu_mem_utils = [m[2] for m in self.metrics]
vram_used = [m[3] for m in self.metrics]
vram_free = [m[4] for m in self.metrics]
temps = [m[5] for m in self.metrics]
powers = [m[6] for m in self.metrics]
clock_graphics = [m[7] for m in self.metrics]
clock_memory = [m[8] for m in self.metrics]
fan_speeds = [m[9] for m in self.metrics]
pstates = [m[10] for m in self.metrics]
# Find most common pstate
most_common_pstate = max(set(pstates), key=pstates.count) if pstates else "P0"
return (
mean(gpu_utils),
max(gpu_utils),
min(gpu_utils),
mean(gpu_mem_utils),
max(gpu_mem_utils),
min(gpu_mem_utils),
mean(vram_used),
max(vram_used),
min(vram_used),
mean(vram_free),
max(vram_free),
min(vram_free),
mean(temps),
max(temps),
min(temps),
mean(powers),
max(powers),
min(powers),
mean(clock_graphics),
mean(clock_memory),
mean(fan_speeds),
most_common_pstate,
)

View file

@ -0,0 +1,39 @@
"""Ollama API client for GPU benchmarking.
This module provides a simple Ollama API client for interacting with the
Ollama service during benchmark execution.
"""
from __future__ import annotations
from typing import Any
import requests
class OllamaClient:
"""Simple Ollama API client."""
def __init__(self, base_url: str = "http://localhost:11434") -> None:
"""Initialise Ollama client with base URL and session."""
self.base_url = base_url
self.session = requests.Session()
def generate(self, model: str, prompt: str, context_length: int) -> dict[str, Any]:
"""Generate text using Ollama API.
Returns:
Ollama response as JSON.
"""
response = self.session.post(
f"{self.base_url}/api/generate",
json={
"model": model,
"prompt": prompt,
"stream": False,
"options": {"num_ctx": context_length},
},
timeout=300,
)
response.raise_for_status()
return response.json() or {}

View file

@ -0,0 +1,153 @@
"""Ollama service management for remote instances.
This module provides functionality for managing Ollama service startup
and configuration on remote GPU instances for benchmarking workflows.
"""
from __future__ import annotations
import json
import time
from typing import TYPE_CHECKING
from .logger import logger
if TYPE_CHECKING:
from typing import Any
from .models import RemoteBenchmarkConfig
VastInstance = Any
class OllamaManager:
"""Manages Ollama service startup and configuration on remote instances.
Handles the startup of the pre-installed Ollama service and model deployment
for consistent benchmarking environments.
"""
def __init__(self, config: RemoteBenchmarkConfig) -> None:
"""Initialise Ollama manager with configuration.
Args:
config: Benchmark configuration containing Ollama settings.
"""
self.config = config
def start_and_configure(self, instance: VastInstance) -> str:
"""Start Ollama service and configure environment on remote instance.
Args:
instance: Target Vast.ai instance for configuration.
Returns:
Ollama version string for results folder naming.
"""
logger.info("🧠 Configuring Ollama service...")
# Start Ollama service with environment variables in background
logger.info("⚡ Starting Ollama server in background")
start_command = (
f"cd /app && "
f"OLLAMA_FLASH_ATTENTION={self.config.ollama_flash_attention} "
f"OLLAMA_KEEP_ALIVE={self.config.ollama_keep_alive} "
f"OLLAMA_KV_CACHE_TYPE={self.config.ollama_kv_cache_type} "
f"OLLAMA_MAX_LOADED_MODELS={self.config.ollama_max_loaded_models} "
f"OLLAMA_NUM_GPU={self.config.ollama_num_gpu} "
f"OLLAMA_NUM_PARALLEL={self.config.ollama_num_parallel} "
f"nohup ollama serve > ollama.log 2>&1 & disown"
)
instance.execute_command_background(start_command)
# Wait for Ollama server to be ready and get version
logger.info("⏰ Waiting for server readiness")
version = self._get_ollama_version(instance)
# Pull the model with streaming output for real-time progress monitoring
logger.info("📦 Pulling model %s", self.config.ollama_model)
pull_command = f"ollama pull {self.config.ollama_model}"
instance.execute_command_streaming(
pull_command,
timeout=1200, # 20 minutes for large models
)
# Test generation to ensure model is loaded into VRAM
logger.info("🎯 Running test generation to load model into VRAM")
test_command = (
f"curl -X POST http://localhost:11434/api/generate "
f'-H "Content-Type: application/json" '
f'-d \'{{"model": "{self.config.ollama_model}", '
f'"prompt": "Say hello", '
f'"stream": false, "options": {{"num_predict": 10}}}}\''
)
response = instance.execute_command_capture(test_command, timeout=60)
try:
response_data = json.loads(response)
actual_response = response_data.get("response", "No response field found")
logger.debug("Test generation response: %r", actual_response)
except (json.JSONDecodeError, AttributeError):
logger.debug(
"Test generation response (raw): %s",
response[:100] + "..." if len(response) > 100 else response,
)
logger.info("🎉 Test generation complete - model should now be in VRAM")
logger.info("🚀 Ollama ready for testing with model preloaded")
return version
def _get_ollama_version(self, instance: VastInstance) -> str:
"""Wait for Ollama server to be ready and get its version.
Args:
instance: The remote instance to check.
Returns:
The Ollama version string.
Raises:
RuntimeError: If the server doesn't become ready within the timeout.
"""
logger.info("⏳ Waiting for Ollama server to start...")
max_attempts = 12 # 60 seconds total (5 second intervals)
debug_threshold = 2 # After 10 seconds (2 attempts), check logs
for attempt in range(max_attempts):
try:
version_output = str(
instance.execute_command_capture("ollama --version", timeout=10)
)
if "version is" in version_output:
version = version_output.rsplit("version is", maxsplit=1)[-1].strip()
logger.info("✅ Ollama server ready, version: %s", version)
return version
except RuntimeError as e:
logger.debug(
"Ollama not ready yet (attempt %d/%d): %s", attempt + 1, max_attempts, e
)
# After 10 seconds, check the ollama.log for debugging
if attempt == debug_threshold - 1:
logger.warning("Ollama not responding after 10 seconds, checking logs...")
try:
log_output = instance.execute_command_capture(
"cd /app && cat ollama.log", timeout=5
)
logger.debug("Ollama log contents:\n%s", log_output)
except Exception as log_error:
logger.debug("Could not read ollama.log: %s", log_error)
logger.debug("Waiting 5 seconds before next attempt...")
time.sleep(5)
# Final attempt to get logs before failing
logger.error("Ollama failed to start. Checking final log state...")
try:
final_log = instance.execute_command_capture("cd /app && cat ollama.log", timeout=5)
logger.error("Final ollama.log contents:\n%s", final_log)
except Exception as e:
logger.error("Could not read final ollama.log: %s", e)
msg = "Ollama server failed to become ready after 60 seconds."
raise RuntimeError(msg)

View file

@ -0,0 +1,50 @@
"""Prompt generation utilities for GPU benchmarking.
This module provides the PromptGenerator class for creating test prompts
of different scenarios and context lengths.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import LocalBenchmarkConfig
class PromptGenerator:
"""Generates prompts for different test scenarios.
Creates short prompts for allocation overhead testing and longer prompts
proportional to context length for realistic usage testing.
"""
@staticmethod
def generate(scenario: str, context_length: int, config: LocalBenchmarkConfig) -> str:
"""Generate prompt based on scenario and context length.
Args:
scenario: 'short' or 'half_context'
context_length: Target context window size
config: Benchmark configuration with prompt templates
Returns:
Generated prompt string appropriate for the scenario.
"""
if scenario == "short":
return config.short_prompt
# Half-context scenario
target_tokens = context_length // 2
target_chars = target_tokens * 4 # ~4 chars per token
prompt_parts = [config.short_prompt, "\n\nContext: "]
current_length = len("".join(prompt_parts))
# Add filler to reach target length
while current_length < target_chars:
prompt_parts.append(config.filler_text + " ")
current_length = len("".join(prompt_parts))
prompt_parts.append("\n\nGiven this context, provide a comprehensive response.")
return "".join(prompt_parts)

View file

@ -0,0 +1,288 @@
"""Results management for GPU benchmarking.
This module provides the ResultsManager class for handling benchmark results
storage, file operations, system information capture, and summary generation.
"""
from __future__ import annotations
import csv
import logging
import subprocess
from dataclasses import asdict
from datetime import UTC, datetime
from pathlib import Path
from statistics import mean, stdev
from typing import TYPE_CHECKING
from .logger import logger
from .models import HF_MODEL
if TYPE_CHECKING:
from .models import TestResult
# Constants for summary generation
MIN_RUNS_FOR_STDEV = 2
# Ollama configuration constants
OLLAMA_CONFIG = {
"OLLAMA_FLASH_ATTENTION": "1",
"OLLAMA_KEEP_ALIVE": "10m",
"OLLAMA_KV_CACHE_TYPE": "q8_0",
"OLLAMA_MAX_LOADED_MODELS": "1",
"OLLAMA_NUM_GPU": "1",
"OLLAMA_NUM_PARALLEL": "1",
}
class ResultsManager:
"""Manages benchmark results storage, file operations, and summary generation.
Handles CSV export, system information capture, logging setup, and
statistical analysis of benchmark results.
"""
def __init__(self, base_dir: Path) -> None:
"""Initialise results manager with base directory for results storage."""
self.base_dir = base_dir
self.base_dir.mkdir(parents=True, exist_ok=True)
self.results: list[TestResult] = []
# Use the shared logger - no need to reconfigure it
self.logger = logger
# Add file handler for benchmark log only
file_handler = logging.FileHandler(self.base_dir / "benchmark.log")
file_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
self.logger.addHandler(file_handler)
def save_system_info(self) -> None:
"""Capture and save comprehensive system information.
Collects GPU specs, CPU info, memory details, software versions,
and Ollama configuration for reproducibility and analysis context.
"""
info_file = self.base_dir / "system_info.txt"
try:
with Path(info_file).open("w", encoding="utf-8") as f:
f.write("=== System Information ===\n")
f.write(f"Date: {datetime.now(tz=UTC)}\n\n")
# Ollama configuration
f.write("=== Ollama Configuration ===\n")
f.writelines(f"{key}: {value}\n" for key, value in OLLAMA_CONFIG.items())
f.write(f"Model: {HF_MODEL}\n")
# Ollama version
try:
ollama_version = subprocess.run(
["ollama", "--version"],
check=False,
capture_output=True,
text=True,
timeout=10,
)
f.write(f"Ollama Version: {ollama_version.stdout.strip()}\n\n")
except subprocess.TimeoutExpired:
f.write("Ollama Version: timeout\n\n")
except Exception as e:
f.write(f"Ollama Version: error - {e}\n\n")
# GPU info
try:
gpu_info = subprocess.run(
["nvidia-smi", "-q"],
check=False,
capture_output=True,
text=True,
timeout=10,
)
f.write("=== GPU Information ===\n")
f.write(gpu_info.stdout)
except subprocess.TimeoutExpired:
f.write("GPU information timeout\n")
# System info
for cmd, label in [
(["lscpu"], "CPU Information"),
(["free", "-h"], "Memory Information"),
]:
try:
result = subprocess.run(
cmd, check=False, capture_output=True, text=True, timeout=5
)
f.write(f"\n=== {label} ===\n")
f.write(result.stdout)
except subprocess.TimeoutExpired:
f.write(f"\n{label} timeout\n")
except Exception:
self.logger.exception("Failed to save system info")
def add_result(self, result: TestResult) -> None:
"""Add a test result to the collection."""
self.results.append(result)
def save_results(self) -> None:
"""Save all results to CSV files.
Exports individual test results and generates summary statistics.
"""
# Individual results
individual_file = self.base_dir / "individual_results.csv"
with Path(individual_file).open("w", encoding="utf-8", newline="") as f:
if self.results:
writer = csv.DictWriter(f, fieldnames=asdict(self.results[0]).keys())
writer.writeheader()
for result in self.results:
writer.writerow(asdict(result))
# Summary statistics
self._save_summary_stats()
def _save_summary_stats(self) -> None:
"""Generate and save summary statistics grouped by scenario and context length.
Calculates means and standard deviations for all metrics across test runs.
"""
summary_file = self.base_dir / "summary_stats.csv"
# Group results by scenario and context length
groups: dict[tuple[str, int], list[TestResult]] = {}
for result in self.results:
key = (result.scenario, result.context_length)
groups.setdefault(key, []).append(result)
with Path(summary_file).open("w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"scenario",
"context_length",
"avg_tokens_per_second",
"std_tokens_per_second",
"avg_gpu_util",
"std_gpu_util",
"avg_gpu_mem_util",
"std_gpu_mem_util",
"avg_vram_used_gb",
"std_vram_used_gb",
"avg_vram_free_gb",
"std_vram_free_gb",
"avg_eval_time_ms",
"std_eval_time_ms",
"avg_response_length",
"avg_temp_c",
"avg_power_w",
"avg_graphics_clock_mhz",
"avg_memory_clock_mhz",
"run_count",
])
for (scenario, context_length), results in sorted(groups.items()):
if len(results) < MIN_RUNS_FOR_STDEV:
continue
tokens_ps = [r.tokens_per_second for r in results]
gpu_utils = [r.gpu_util_avg for r in results]
gpu_mem_utils = [r.gpu_mem_util_avg for r in results]
vrams_used = [r.vram_used_avg_mb / 1024 for r in results] # Convert to GB
vrams_free = [r.vram_free_avg_mb / 1024 for r in results] # Convert to GB
eval_times = [r.eval_duration_ms for r in results]
response_lens = [r.response_length_chars for r in results]
temps = [r.gpu_temp_avg for r in results]
powers = [r.gpu_power_avg for r in results]
graphics_clocks = [r.gpu_clock_graphics_avg for r in results]
memory_clocks = [r.gpu_clock_memory_avg for r in results]
writer.writerow([
scenario,
context_length,
round(mean(tokens_ps), 3),
round(stdev(tokens_ps), 3),
round(mean(gpu_utils), 1),
round(stdev(gpu_utils), 1),
round(mean(gpu_mem_utils), 1),
round(stdev(gpu_mem_utils), 1),
round(mean(vrams_used), 2),
round(stdev(vrams_used), 2),
round(mean(vrams_free), 2),
round(stdev(vrams_free), 2),
round(mean(eval_times), 1),
round(stdev(eval_times), 1),
round(mean(response_lens)),
round(mean(temps), 1),
round(mean(powers), 1),
round(mean(graphics_clocks)),
round(mean(memory_clocks)),
len(results),
])
def print_summary(self) -> None:
"""Print comprehensive benchmark summary to console.
Displays results grouped by scenario with statistical analysis
and validates benchmark completion status.
"""
if not self.results:
logger.warning("No results to summarise")
return
# Group by scenario
scenarios: dict[str, list[TestResult]] = {}
for result in self.results:
scenarios.setdefault(result.scenario, []).append(result)
logger.info("\n=== BENCHMARK COMPLETE ===")
logger.info("=== FULL BENCHMARK RESULTS ===")
for scenario, results in scenarios.items():
logger.info("=== %s SCENARIO RESULTS ===", scenario.upper())
logger.info(
"Context Length | Tokens/sec (±SD) | GPU Util% (±SD) | Mem Util% (±SD) | "
"VRAM Used GB (±SD)"
)
# Group by context length
contexts: dict[int, list[TestResult]] = {}
for result in results:
contexts.setdefault(result.context_length, []).append(result)
for context_length in sorted(contexts.keys()):
ctx_results = contexts[context_length]
tokens_ps = [r.tokens_per_second for r in ctx_results]
gpu_utils = [r.gpu_util_avg for r in ctx_results]
gpu_mem_utils = [r.gpu_mem_util_avg for r in ctx_results]
vrams_used = [r.vram_used_avg_mb / 1024 for r in ctx_results]
if len(ctx_results) > 1:
tokens_mean, tokens_std = mean(tokens_ps), stdev(tokens_ps)
gpu_mean, gpu_std = mean(gpu_utils), stdev(gpu_utils)
gpu_mem_mean, gpu_mem_std = (
mean(gpu_mem_utils),
stdev(gpu_mem_utils),
)
vram_mean, vram_std = mean(vrams_used), stdev(vrams_used)
else:
tokens_mean, tokens_std = tokens_ps[0], 0
gpu_mean, gpu_std = gpu_utils[0], 0
gpu_mem_mean, gpu_mem_std = gpu_mem_utils[0], 0
vram_mean, vram_std = vrams_used[0], 0
logger.info(
"%11s | %8.1f±%.1f | %8.1f±%.1f | %8.1f±%.1f | %7.2f±%.2f",
context_length,
tokens_mean,
tokens_std,
gpu_mean,
gpu_std,
gpu_mem_mean,
gpu_mem_std,
vram_mean,
vram_std,
)
logger.info("✓ Full benchmark suite complete!")
logger.info("✓ Ready for community analysis and sharing")

View file

@ -0,0 +1,233 @@
"""Direct API client for Vast.ai operations.
This module provides a direct API client for interacting with Vast.ai services,
including instance management, offer searching, and command execution.
"""
from __future__ import annotations
import json
from typing import Any
import requests
from .logger import logger
class VastAPIError(Exception):
"""Custom exception for known Vast.ai API errors that should be handled gracefully."""
def __init__(self, message: str, error_type: str = "unknown") -> None:
"""Initialise VastAPIError with message and error type.
Args:
message: The user-friendly error message.
error_type: The type of error for categorisation.
"""
super().__init__(message)
self.error_type = error_type
class VastAIDirect:
"""Direct API implementation for Vast.ai operations."""
def __init__(self, api_key: str) -> None:
"""Initialise Vast.ai direct API client."""
self.api_key = api_key
self.base_url = "https://console.vast.ai/api/v0"
self.headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
self.timeout = 30
def search_offers(
self, gpu_name: str, num_gpus: int = 1, disk_space: float = 30.0
) -> list[dict[str, Any]]:
"""Search for available GPU offers.
Returns:
List of available GPU offers.
"""
params: dict[str, str | int] = {
"q": json.dumps({
"gpu_name": {"eq": gpu_name},
"num_gpus": {"eq": num_gpus},
"disk_space": {"gte": disk_space},
"rentable": {"eq": True},
}),
"order": "score-",
"limit": 10,
}
logger.debug("[API] GET %s/bundles - Searching for GPU offers", self.base_url)
response = requests.get(
f"{self.base_url}/bundles", headers=self.headers, params=params, timeout=self.timeout
)
if not response.ok:
logger.error("Search failed: %s - %s", response.status_code, response.text)
return []
data = response.json()
if isinstance(data, list):
return data
return list(data.get("offers", []))
def create_instance(
self, offer_id: int | None, image: str, disk: float = 30.0, onstart_cmd: str | None = None
) -> dict[str, Any]:
"""Create an instance from an offer.
Returns:
Dictionary containing the instance details.
"""
payload = {
"id": offer_id,
"image": image,
"disk": disk,
"ssh": True,
}
if onstart_cmd:
payload["onstart_cmd"] = onstart_cmd
logger.debug(
"[API] PUT %s/asks/%s/ - Creating instance from offer", self.base_url, offer_id
)
response = requests.put(
f"{self.base_url}/asks/{offer_id}/", headers=self.headers, json=payload, timeout=60
)
if not response.ok:
# Debug log the raw API response for troubleshooting
logger.debug("Raw API error response: %s", response.text)
error_msg = self._parse_api_error(response)
return {"error": error_msg, "status_code": response.status_code}
data: dict[str, Any] = response.json()
return data
def _parse_api_error(self, response: requests.Response) -> str:
"""Parse API error response and provide user-friendly messages.
Args:
response: The failed HTTP response.
Returns:
A user-friendly error message.
"""
try:
error_data = response.json()
error_type = error_data.get("error", "unknown_error")
error_msg = error_data.get(
"msg", error_data.get("message", "No error message provided")
)
# Handle specific error types with helpful guidance
if error_type == "insufficient_credit":
return (
f"❌ Insufficient credit in your Vast.ai account.\n"
f"💡 Please add credit to your account at: https://console.vast.ai/billing/\n"
f" Original message: {error_msg}"
)
if error_type == "bad_request":
return (
f"❌ Invalid request parameters.\n"
f"💡 Check your GPU configuration and ensure the instance is still available.\n"
f" Original message: {error_msg}"
)
if error_type == "offer_unavailable":
return (
f"❌ The selected GPU offer is no longer available.\n"
f"💡 Try running the script again to find new offers.\n"
f" Original message: {error_msg}"
)
except (json.JSONDecodeError, AttributeError):
# Fallback if response isn't valid JSON
return f"❌ HTTP {response.status_code}: {response.text}"
else:
return f"❌ API Error ({error_type}): {error_msg}"
def get_instance_status(self, instance_id: str) -> dict[str, Any]:
"""Get status of an instance.
Returns:
Dictionary containing the instance status.
"""
logger.debug(
"[API] GET %s/instances/%s/ - Getting instance status", self.base_url, instance_id
)
response = requests.get(
f"{self.base_url}/instances/{instance_id}/", headers=self.headers, timeout=self.timeout
)
if not response.ok:
return {"error": response.text, "status_code": response.status_code}
data: dict[str, Any] = response.json()
return data
def get_ssh_connection(self, instance_id: str) -> dict[str, Any]:
"""Get SSH connection details for an instance.
Returns:
Dictionary containing SSH connection details including host and port.
"""
logger.debug(
"[API] GET %s/instances/%s/ssh/ - Getting SSH connection details",
self.base_url,
instance_id,
)
response = requests.get(
f"{self.base_url}/instances/{instance_id}/ssh/",
headers=self.headers,
timeout=self.timeout,
)
if not response.ok:
return {"error": response.text, "status_code": response.status_code}
data: dict[str, Any] = response.json()
return data
def destroy_instance(self, instance_id: str) -> bool:
"""Destroy an instance.
Returns:
True if the instance was destroyed successfully, False otherwise.
"""
logger.debug(
"[API] DELETE %s/instances/%s/ - Destroying instance", self.base_url, instance_id
)
response = requests.delete(
f"{self.base_url}/instances/{instance_id}/", headers=self.headers, timeout=self.timeout
)
return response.ok
def execute_command(self, instance_id: str, command: str) -> dict[str, Any]:
"""Execute a command on an instance.
Returns:
Dictionary containing the command result.
"""
logger.debug(
"[API] POST %s/instances/%s/execute/ - Executing command on instance",
self.base_url,
instance_id,
)
response = requests.post(
f"{self.base_url}/instances/{instance_id}/execute/",
headers=self.headers,
json={"command": command},
timeout=120,
)
if not response.ok:
return {"error": response.text, "status_code": response.status_code}
try:
data: dict[str, Any] = response.json()
except json.JSONDecodeError:
# Handle cases where response isn't valid JSON
return {"error": "Invalid JSON response", "raw_response": response.text}
else:
return data

View file

@ -0,0 +1,518 @@
"""Vast.ai instance management for remote GPU benchmarking.
This module provides the VastInstance class that handles the complete lifecycle
of Vast.ai GPU instances including provisioning, status monitoring, SSH connectivity,
and cleanup operations for benchmarking workflows.
"""
from __future__ import annotations
import json
import time
from typing import TYPE_CHECKING, Any
import requests
from .interactive import SSHInteractiveSession
from .logger import logger
from .vastai_api import VastAIDirect, VastAPIError
if TYPE_CHECKING:
from pathlib import Path
from .models import RemoteBenchmarkConfig
class VastInstance:
"""Represents a Vast.ai GPU instance with lifecycle management."""
def __init__(self, client: VastAIDirect, config: RemoteBenchmarkConfig) -> None:
"""Initialise Vast.ai instance manager."""
self.client = client
self.config = config
self.instance_id: str | None = None
self.ssh_host: str | None = None
self.ssh_port: int | None = None
self.ssh_user: str = "root"
self.ssh_session: SSHInteractiveSession | None = None
def launch(self) -> None:
"""Launch and configure a new GPU instance."""
logger.info("🚀 Launching %sx %s instance...", self.config.num_gpus, self.config.gpu_type)
self._create_instance_with_retry()
instance_data = self._wait_for_readiness()
self._setup_ssh_connection(instance_data)
def _create_instance_with_retry(self) -> None:
"""Create instance with retry logic for offer availability.
Raises:
RuntimeError: If the instance cannot be created.
"""
max_attempts = 3
for attempt in range(max_attempts):
logger.info(
"✨ Searching for available offers (attempt %d/%d)...", attempt + 1, max_attempts
)
offer = self._find_best_offer()
if self._try_create_instance(offer, attempt, max_attempts):
return
msg = f"Failed to launch instance after {max_attempts} attempts"
raise RuntimeError(msg)
def _find_best_offer(self) -> dict[str, Any]:
"""Find the best available offer for the configured GPU type.
Returns:
The best offer found.
"""
offers = self.client.search_offers(
gpu_name=self.config.gpu_type,
num_gpus=self.config.num_gpus,
disk_space=float(self.config.disk_space),
)
if not offers:
self._raise_no_offers_error()
logger.info("🎯 Found %s offers, trying best offer", len(offers))
return offers[0]
def _raise_no_offers_error(self) -> None:
"""Raise informative error when no offers are available.
Raises:
RuntimeError: Always, with a descriptive message.
"""
logger.info("No offers found, checking available GPU types...")
available_gpus = self._get_available_gpu_types()
msg = f"No verified offers found for {self.config.num_gpus}x {self.config.gpu_type}"
if available_gpus:
msg += f"\n\nAvailable GPU types: {', '.join(sorted(available_gpus))}"
msg += "\n\nPlease update GPU_TYPE in your .env file to one of the available types."
raise RuntimeError(msg)
def _try_create_instance(self, offer: dict[str, Any], attempt: int, max_attempts: int) -> bool:
"""Try to create instance from offer.
Returns:
True if the instance was created successfully, False otherwise.
Raises:
RuntimeError: If instance creation fails after all retries.
VastAPIError: If there is an API-level error from Vast.ai.
"""
offer_id = offer.get("id")
if offer_id is None:
logger.warning("Offer has no ID, skipping")
return False
logger.info(
"💰 Selected offer: ID=%s, Price=$%.3f/hr, Location=%s",
offer_id,
offer.get("dph_total", 0),
offer.get("geolocation", "Unknown"),
)
try:
result = self.client.create_instance(
offer_id=offer_id,
image="ghcr.io/tcpipuk/gpu-llm-benchmarking:latest",
disk=float(self.config.disk_space),
)
if "error" in result:
return self._handle_creation_error(
result["error"], offer, offer_id, attempt, max_attempts
)
self.instance_id = str(result.get("new_contract"))
logger.info("✅ Instance %s created successfully.", self.instance_id)
except VastAPIError:
raise
except Exception as e:
if attempt < max_attempts - 1:
logger.warning("Instance creation failed, retrying: %s", e)
time.sleep(2)
return False
msg = "Failed to launch instance after all retry attempts"
raise RuntimeError(msg) from e
else:
return True
def _handle_creation_error(
self, error_msg: str, offer: dict[str, Any], offer_id: str, attempt: int, max_attempts: int
) -> bool:
"""Handle instance creation errors with appropriate retry logic.
Returns:
False if the error can be retried, otherwise raises an exception.
Raises:
RuntimeError: For unrecoverable errors.
VastAPIError: For specific API errors like insufficient credit.
"""
if "credit" in error_msg.lower() and "insufficient" in error_msg.lower():
msg = (
f"💳 Cannot launch GPU instance due to insufficient credit.\n\n"
f"Selected offer details:\n"
f" • GPU: {self.config.num_gpus}x {self.config.gpu_type}\n"
f" • Price: ${offer.get('dph_total', 0):.3f}/hour\n"
f" • Location: {offer.get('geolocation', 'Unknown')}\n\n"
f"Please add credit to your Vast.ai account and try again.\n"
f"💡 Billing page: https://console.vast.ai/billing/"
)
raise VastAPIError(msg, "insufficient_credit")
if "no_such_ask" in error_msg.lower() and attempt < max_attempts - 1:
logger.warning("Offer %s no longer available, searching for new offers...", offer_id)
time.sleep(2)
return False
if any(
known_error in error_msg.lower() for known_error in ["bad_request", "offer_unavailable"]
):
raise VastAPIError(error_msg, "api_error")
msg = f"Failed to create instance: {error_msg}"
raise RuntimeError(msg)
def _wait_for_readiness(self) -> dict[str, Any]:
"""Wait for instance to become ready for use.
Returns:
The instance data when ready.
Raises:
RuntimeError: If the instance fails to become ready within the timeout.
"""
logger.info("⏳ Waiting for instance to become ready...")
max_wait_time = 600
start_time = time.time()
last_status = None
# Initial delay to allow instance to start up before first API check
time.sleep(20)
while time.time() - start_time < max_wait_time:
try:
if self.instance_id is None:
msg = "Instance ID is None"
raise RuntimeError(msg)
status = self.client.get_instance_status(self.instance_id)
instance_data: dict[str, Any] = status.get("instances", status)
actual_status = instance_data.get("actual_status")
if actual_status != last_status:
if actual_status is None:
logger.info("⏳ Instance initialising...")
elif actual_status == "loading":
logger.info("🐳 Loading Docker image...")
elif actual_status == "running":
logger.info("🌟 Instance is running")
self._log_instance_metadata(instance_data)
return instance_data
else:
logger.info("Instance status: %s", actual_status)
last_status = actual_status
if "error" not in status and actual_status == "running":
return instance_data
except Exception as e:
logger.warning("Error checking instance status: %s", e)
time.sleep(20)
self.destroy()
msg = "Instance failed to become ready within timeout"
raise RuntimeError(msg)
def _log_instance_metadata(self, instance_data: dict[str, Any]) -> None:
"""Log useful instance metadata for analysis and debugging."""
metadata = {
"gpu_name": instance_data.get("gpu_name"),
"gpu_ram": instance_data.get("gpu_ram"),
"cpu_name": instance_data.get("cpu_name"),
"cpu_cores": instance_data.get("cpu_cores"),
"cpu_ram": instance_data.get("cpu_ram"),
"location": instance_data.get("geolocation"),
"host_id": instance_data.get("host_id"),
"machine_id": instance_data.get("machine_id"),
"driver_version": instance_data.get("driver_version"),
"cost_per_hour": instance_data.get("dph_total"),
"disk_name": instance_data.get("disk_name"),
"disk_bw": instance_data.get("disk_bw"),
"compute_cap": instance_data.get("compute_cap"),
"pcie_bw": instance_data.get("pcie_bw"),
"reliability": instance_data.get("reliability2"),
}
logger.info("💻 Instance Details:")
logger.info(" GPU: %s (%s MB VRAM)", metadata["gpu_name"], metadata["gpu_ram"])
logger.info(
" CPU: %s (%s cores, %s MB RAM)",
metadata["cpu_name"],
metadata["cpu_cores"],
metadata["cpu_ram"],
)
logger.info(
" Location: %s (Host: %s, Machine: %s)",
metadata["location"],
metadata["host_id"],
metadata["machine_id"],
)
logger.info(
" Cost: $%.3f/hour | Driver: %s | Reliability: %.1f%%",
metadata["cost_per_hour"] or 0,
metadata["driver_version"],
(metadata["reliability"] or 0) * 100,
)
logger.info(
" Storage: %s (%.1f MB/s) | Compute: %s | PCIe: %.1f GB/s",
metadata["disk_name"],
metadata["disk_bw"] or 0,
metadata["compute_cap"],
metadata["pcie_bw"] or 0,
)
def _setup_ssh_connection(self, instance_data: dict[str, Any] | None = None) -> None:
"""Extract and store SSH connection details and test connectivity.
Args:
instance_data: Optional instance data from previous API call to avoid
redundant requests.
Raises:
RuntimeError: If SSH details cannot be retrieved.
"""
logger.info("🔐 Getting SSH connection details...")
try:
if self.instance_id is None:
msg = "Instance ID is None"
raise RuntimeError(msg)
# Use provided instance data or fetch it
if instance_data is None:
status = self.client.get_instance_status(self.instance_id)
if "error" in status:
msg = f"Failed to get instance status: {status}"
raise RuntimeError(msg)
instance_data = status.get("instances", status)
self.ssh_host = instance_data.get("ssh_host") or instance_data.get("public_ipaddr")
self.ssh_port = int(instance_data.get("ssh_port", 22))
if not self.ssh_host:
msg = "No SSH host found in instance status"
raise RuntimeError(msg)
logger.info("💻 SSH connection: %s@%s:%s", self.ssh_user, self.ssh_host, self.ssh_port)
self._wait_for_ssh_ready()
except Exception as e:
msg = "Failed to get SSH connection details"
raise RuntimeError(msg) from e
def _wait_for_ssh_ready(self) -> None:
"""Wait for SSH to become available and establish a persistent session.
Raises:
RuntimeError: If the SSH connection fails.
"""
logger.info("🔑 Waiting for SSH access...")
if not self.ssh_host or not self.ssh_port:
msg = "SSH host/port not set."
raise RuntimeError(msg)
self.ssh_session = SSHInteractiveSession(self.ssh_host, self.ssh_port, self.ssh_user)
# Retry SSH connection with exponential backoff
max_retries = 10
base_delay = 2
for attempt in range(max_retries):
try:
self.ssh_session.connect()
except Exception as e:
if attempt < max_retries - 1:
delay = base_delay * (2**attempt)
logger.warning(
"SSH connection attempt %d/%d failed: %s. Retrying in %d seconds...",
attempt + 1,
max_retries,
e,
delay,
)
time.sleep(delay)
else:
self.destroy()
msg = f"SSH connection failed after {max_retries} attempts."
raise RuntimeError(msg) from e
else:
logger.info("✅ SSH connection established")
return
def transfer_file(self, local_path: str, remote_path: str) -> None:
"""Transfer a file to the remote instance using SFTP.
Raises:
RuntimeError: If the file transfer fails.
"""
if not self.ssh_session or not self.ssh_session.sftp:
msg = "SFTP session not established."
raise RuntimeError(msg)
logger.info("Transferring %s to instance...", local_path)
try:
self.ssh_session.sftp.put(local_path, remote_path)
logger.info("File transferred successfully.")
except Exception as e:
self.destroy()
msg = f"SFTP upload failed: {e}"
raise RuntimeError(msg) from e
def download_results(self, remote_path: str, local_path: Path) -> None:
"""Download benchmark results from remote instance using SFTP.".
Raises:
RuntimeError: If the SFTP session is not established.
"""
if not self.ssh_session or not self.ssh_session.sftp:
msg = "SFTP session not established."
raise RuntimeError(msg)
local_path.mkdir(parents=True, exist_ok=True)
logger.info("Transferring results to %s...", local_path)
try:
# List contents of the remote directory
remote_files = self.ssh_session.sftp.listdir(remote_path)
for filename in remote_files:
remote_filepath = f"{remote_path}/{filename}"
local_filepath = local_path / filename
self.ssh_session.sftp.get(remote_filepath, str(local_filepath))
logger.info("Results transferred successfully.")
except Exception as e:
logger.error("SFTP download failed: %s", e)
def execute_command_interactive(
self, command: str, _description: str = "", timeout: int = 600
) -> None:
"""Execute a command interactively via the persistent SSH session.
Raises:
RuntimeError: If the SSH session is not established.
"""
if not self.ssh_session:
msg = "SSH session not established"
raise RuntimeError(msg)
try:
self.ssh_session.execute_command_interactive(command, timeout)
except Exception:
self.destroy()
raise
def execute_command_capture(self, command: str, timeout: int = 30) -> str:
"""Execute a command and capture its output via the persistent SSH session.
Returns:
The command output.
Raises:
RuntimeError: If the SSH session is not established.
"""
if not self.ssh_session:
msg = "SSH session not established"
raise RuntimeError(msg)
try:
return self.ssh_session.execute_command_capture(command, timeout)
except Exception:
self.destroy()
raise
def execute_command_background(self, command: str) -> None:
"""Execute a command in the background without waiting for output.
This method is designed for fire-and-forget commands that run in the
background (e.g., using nohup, &, or disown).
Raises:
RuntimeError: If the SSH session is not established.
"""
if not self.ssh_session:
msg = "SSH session not established"
raise RuntimeError(msg)
try:
self.ssh_session.execute_command_background(command)
except Exception:
self.destroy()
raise
def execute_command_streaming(self, command: str, timeout: int = 600) -> None:
"""Execute a command with real-time output streaming.
This method provides real-time output streaming for long-running commands
like model downloads, showing progress as it happens.
Raises:
RuntimeError: If the SSH session is not established.
"""
if not self.ssh_session:
msg = "SSH session not established"
raise RuntimeError(msg)
try:
self.ssh_session.execute_command_streaming(command, timeout)
except Exception:
self.destroy()
raise
def _get_available_gpu_types(self) -> list[str]:
"""Get a list of available GPU types from Vast.ai offers.
Returns:
A list of available GPU types.
"""
try:
params: dict[str, str | int] = {
"q": json.dumps({"rentable": {"eq": True}}),
"order": "score-",
"limit": 100,
}
response = requests.get(
f"{self.client.base_url}/bundles",
headers=self.client.headers,
params=params,
timeout=self.client.timeout,
)
if not response.ok:
logger.warning("Failed to fetch available GPU types: %s", response.status_code)
return []
data = response.json()
offers = data if isinstance(data, list) else data.get("offers", [])
gpu_types = {offer.get("gpu_name") for offer in offers if offer.get("gpu_name")}
return sorted(gpu_types)
except Exception as e:
logger.warning("Error fetching available GPU types: %s", e)
return []
def destroy(self) -> None:
"""Clean up the instance and close the SSH session."""
if self.ssh_session:
self.ssh_session.close()
self.ssh_session = None
if not self.instance_id:
return
logger.info("Destroying instance...")
max_attempts = 3
for attempt in range(max_attempts):
try:
self.client.destroy_instance(self.instance_id)
except Exception as e:
if attempt < max_attempts - 1:
logger.warning("Destroy attempt %d failed, retrying: %s", attempt + 1, e)
time.sleep(5)
else:
logger.error(
"Failed to destroy instance after %d attempts: %s", max_attempts, e
)
logger.warning(
"⚠️ Instance %s may still be running and incurring charges!",
self.instance_id,
)
else:
logger.info("Instance %s destroyed successfully.", self.instance_id)
return

339
scripts/llm_benchmark.py Normal file
View file

@ -0,0 +1,339 @@
#!/usr/bin/env python3
"""NVIDIA GPU LLM Context Length Benchmark.
Comprehensive benchmarking tool for testing Large Language Model inference performance
across different context window sizes on NVIDIA GPUs. Tests two scenarios: short prompts
(allocation overhead) and half-context prompts (realistic usage) to measure GPU utilization,
memory bandwidth, VRAM usage, and generation speed. Designed to evaluate GPU performance
across different NVIDIA architectures with detailed monitoring of all GPU metrics and
comprehensive data collection for analysis.
Tests across different context windows with two scenarios:
1. Short prompts (allocation overhead testing)
2. Half-context prompts (realistic usage testing)
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import time
from datetime import UTC, datetime
from pathlib import Path
from helpers.logger import logger
from helpers.models import (
CONTEXT_END,
CONTEXT_MULTIPLIER,
CONTEXT_START,
HF_MODEL,
TEST_ITERATIONS,
LocalBenchmarkConfig,
TestResult,
)
from helpers.nvidia_gpu_monitor import GPUMonitor
from helpers.ollama_client import OllamaClient
from helpers.prompt_generator import PromptGenerator
from helpers.results_manager import ResultsManager
OUTPUT_PATH = os.getenv("OUTPUT_PATH", ".") # Directory for benchmark results
# Ollama configuration - critical for reproducible results
OLLAMA_CONFIG = {
"OLLAMA_FLASH_ATTENTION": os.getenv("OLLAMA_FLASH_ATTENTION", "1"),
"OLLAMA_KEEP_ALIVE": os.getenv("OLLAMA_KEEP_ALIVE", "10m"),
"OLLAMA_KV_CACHE_TYPE": os.getenv("OLLAMA_KV_CACHE_TYPE", "q8_0"),
"OLLAMA_MAX_LOADED_MODELS": os.getenv("OLLAMA_MAX_LOADED_MODELS", "1"),
"OLLAMA_NUM_GPU": os.getenv("OLLAMA_NUM_GPU", "1"),
"OLLAMA_NUM_PARALLEL": os.getenv("OLLAMA_NUM_PARALLEL", "1"),
}
# Logger imported from helpers.logger
# Remaining constants
MIN_RUNS_FOR_STDEV = 2
class BenchmarkRunner:
"""Main benchmark execution coordinator.
Orchestrates the complete benchmark process including environment setup,
model warmup, test execution, and results collection across all scenarios
and context lengths.
"""
def __init__(self, config: LocalBenchmarkConfig) -> None:
"""Initialise benchmark runner with configuration and results manager."""
self.config = config
timestamp = datetime.now(tz=UTC).strftime("%Y%m%d_%H%M%S")
output_dir = Path(OUTPUT_PATH)
self.results_dir = output_dir / f"benchmark_results_{timestamp}"
self.results_manager = ResultsManager(self.results_dir)
self.ollama_client = OllamaClient()
logger.info("🎯 Benchmark initialised")
logger.info("📁 Results directory: %s", self.results_dir)
logger.info("🚀 Running FULL benchmark suite")
def setup_environment(self) -> None:
"""Setup environment for benchmark execution.
Assumes Ollama is already installed and running with the model loaded.
Logs version for reproducibility and verifies connection.
Raises:
RuntimeError: If Ollama is not available or model not loaded.
"""
logger.info("⚙️ Setting up benchmark environment...")
# Log Ollama version for reproducibility
try:
version_result = subprocess.run(
["ollama", "--version"], capture_output=True, text=True, check=True
)
logger.info("📝 Ollama version: %s", version_result.stdout.strip())
except subprocess.CalledProcessError as e:
logger.warning("⚠️ Could not get Ollama version: %s", e)
# Verify Ollama is running by testing connection
logger.info("🔗 Verifying Ollama connection...")
try:
warmup_response = self.ollama_client.generate(
model=self.config.model,
prompt="Hello",
context_length=self.config.context_start,
)
warmup_tokens = warmup_response.get("eval_count", 0)
logger.info("✅ Ollama connection verified - generated %s tokens", warmup_tokens)
except Exception as e:
logger.exception("❌ Ollama connection failed")
msg = "Ollama is not available or model not loaded"
raise RuntimeError(msg) from e
# Save system info
self.results_manager.save_system_info()
def run_single_test(self, scenario: str, context_length: int, run_number: int) -> TestResult:
"""Run a single benchmark test with comprehensive monitoring.
Args:
scenario: Test scenario ('short' or 'half_context')
context_length: Context window size for this test
run_number: Current run iteration number
Returns:
TestResult object containing all metrics and timing data.
"""
test_id = f"{scenario}_{context_length}_{run_number}_{int(time.time())}"
logger.info(
"🧪 Running test: %s scenario, %s context, run %s/%s",
scenario,
context_length,
run_number,
self.config.runs_per_context,
)
# Generate prompt and save it
prompt = PromptGenerator.generate(scenario, context_length, self.config)
prompt_file = self.results_dir / f"prompt_{test_id}.txt"
prompt_file.write_text(prompt)
# Setup GPU monitoring
monitor_file = self.results_dir / f"gpu_monitor_{test_id}.csv"
gpu_monitor = GPUMonitor(monitor_file)
# Run test with monitoring
start_time = time.time()
with gpu_monitor.monitor():
ollama_response = self.ollama_client.generate(
model=self.config.model, prompt=prompt, context_length=context_length
)
end_time = time.time()
# Save full response
response_file = self.results_dir / f"ollama_response_{test_id}.json"
response_file.write_text(json.dumps(ollama_response, indent=2))
# Save generated text
if "response" in ollama_response:
text_file = self.results_dir / f"response_{test_id}.txt"
text_file.write_text(ollama_response["response"])
# Calculate metrics
eval_count = ollama_response.get("eval_count", 0)
eval_duration = ollama_response.get("eval_duration", 0)
tokens_per_second = (eval_count * 1_000_000_000 / eval_duration) if eval_duration > 0 else 0
gpu_stats = gpu_monitor.get_stats()
# Create result with expanded GPU metrics
result = TestResult(
timestamp=datetime.now(tz=UTC).isoformat(),
scenario=scenario,
context_length=context_length,
run_number=run_number,
tokens_per_second=tokens_per_second,
total_time_seconds=end_time - start_time,
prompt_eval_count=ollama_response.get("prompt_eval_count", 0),
prompt_eval_duration_ms=ollama_response.get("prompt_eval_duration", 0) / 1_000_000,
eval_count=eval_count,
eval_duration_ms=eval_duration / 1_000_000,
total_duration_ms=ollama_response.get("total_duration", 0) / 1_000_000,
load_duration_ms=ollama_response.get("load_duration", 0) / 1_000_000,
response_length_chars=len(ollama_response.get("response", "")),
gpu_util_avg=gpu_stats[0],
gpu_util_max=gpu_stats[1],
gpu_util_min=gpu_stats[2],
gpu_mem_util_avg=gpu_stats[3],
gpu_mem_util_max=gpu_stats[4],
gpu_mem_util_min=gpu_stats[5],
vram_used_avg_mb=gpu_stats[6],
vram_used_max_mb=gpu_stats[7],
vram_used_min_mb=gpu_stats[8],
vram_free_avg_mb=gpu_stats[9],
vram_free_max_mb=gpu_stats[10],
vram_free_min_mb=gpu_stats[11],
gpu_temp_avg=gpu_stats[12],
gpu_temp_max=gpu_stats[13],
gpu_temp_min=gpu_stats[14],
gpu_power_avg=gpu_stats[15],
gpu_power_max=gpu_stats[16],
gpu_power_min=gpu_stats[17],
gpu_clock_graphics_avg=gpu_stats[18],
gpu_clock_memory_avg=gpu_stats[19],
gpu_fan_speed_avg=gpu_stats[20],
gpu_pstate_mode=gpu_stats[21],
test_id=test_id,
)
logger.info(
" ✅ Generated %s tokens in %.1fms (%.1f t/s) | GPU: %.1f%% | "
"MemBW: %.1f%% | VRAM: %.1fGB",
eval_count,
eval_duration / 1_000_000,
tokens_per_second,
gpu_stats[0],
gpu_stats[3],
gpu_stats[6] / 1024,
)
return result
def run_benchmark(self) -> None:
"""Run complete benchmark suite across all scenarios and context lengths.
Executes all configured test combinations, collects results,
and generates comprehensive summary output.
"""
total_tests = (
len(self.config.scenarios or [])
* len(self.config.context_lengths or [])
* self.config.runs_per_context
)
logger.info("🎬 Starting benchmark with %s total tests", total_tests)
test_count = 0
for scenario in self.config.scenarios or []:
logger.info("🎭 === %s SCENARIO ===", scenario.upper())
for context_length in self.config.context_lengths or []:
logger.info("📏 --- Context Length: %s ---", context_length)
for run_number in range(1, self.config.runs_per_context + 1):
test_count += 1
try:
result = self.run_single_test(scenario, context_length, run_number)
self.results_manager.add_result(result)
logger.info("✅ Test %s/%s complete", test_count, total_tests)
# Brief pause between tests
time.sleep(3)
except Exception:
logger.exception("❌ Test failed")
continue
# Save results and print summary
self.results_manager.save_results()
self.results_manager.print_summary()
logger.info("🎉 Full benchmark complete! Results saved to: %s", self.results_dir)
logger.info("📊 Complete dataset ready for community sharing")
def parse_arguments() -> argparse.Namespace:
"""Parse command line arguments for benchmark configuration.
Returns:
Parsed arguments namespace.
"""
parser = argparse.ArgumentParser(
description="LLM Benchmark with Context Length",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
Configuration (edit globals at top of script):
Model: {HF_MODEL}
Iterations per test: {TEST_ITERATIONS}
Context range: {CONTEXT_START} {CONTEXT_END} (x{CONTEXT_MULTIPLIER} each step)
Output path: {OUTPUT_PATH}
""",
)
return parser.parse_args()
def main() -> None:
"""Main entry point for benchmark execution.
Initialises configuration and runs the complete benchmark suite with error handling and logging.
"""
parse_arguments() # Parse arguments for help message support
config = LocalBenchmarkConfig()
runner = BenchmarkRunner(config)
# Log configuration
total_tests = (
len(config.scenarios or []) * len(config.context_lengths or []) * config.runs_per_context
)
logger.info("🚀 Starting full benchmark suite")
logger.info("🤖 Model: %s", config.model)
logger.info("📏 Context lengths: %s", config.context_lengths)
logger.info("🔄 Iterations per context: %s", config.runs_per_context)
logger.info("📁 Output path: %s", OUTPUT_PATH)
logger.info("🧮 Total tests: %s", total_tests)
# Log Ollama configuration
logger.info("⚙️ Ollama configuration:")
for key, value in OLLAMA_CONFIG.items():
logger.info(" %s: %s", key, value)
logger.info(
"📊 Testing %s%s (x%s progression)",
config.context_start,
config.context_end,
config.context_multiplier,
)
try:
runner.setup_environment()
runner.run_benchmark()
except KeyboardInterrupt:
logger.info("⏹️ Benchmark interrupted by user")
except Exception:
logger.exception("💥 Benchmark failed")
raise
if __name__ == "__main__":
main()

View file

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Remote GPU LLM Benchmarking Orchestrator.
Automated orchestration tool for executing LLM context length benchmarks on remote
Vast.ai GPU instances. This script handles the complete lifecycle of remote benchmarking:
instance provisioning, Ollama installation and configuration, benchmark execution,
results retrieval, and resource cleanup.
The orchestrator supports configurable GPU types, Ollama versions, and benchmark
parameters through environment variables. It provides comprehensive logging and
error handling for reliable automated benchmarking workflows across different
GPU architectures and configurations.
Key Features:
- Automated Vast.ai instance lifecycle management
- Configurable Ollama installation and model deployment
- Secure file transfer for scripts and results
- Comprehensive error handling and cleanup
- Environment-based configuration management
"""
from __future__ import annotations
from helpers.logger import logger
from helpers.models import RemoteBenchmarkConfig
from helpers.ollama_manager import OllamaManager
from helpers.vastai_api import VastAIDirect, VastAPIError
from helpers.vastai_instance import VastInstance
class BenchmarkExecutor:
"""Executes benchmark scripts on remote instances with proper configuration.
Coordinates the execution of LLM benchmarks with environment variable
configuration and result collection.
"""
def __init__(self, config: RemoteBenchmarkConfig) -> None:
"""Initialise benchmark executor with configuration.
Args:
config: Benchmark configuration settings.
"""
self.config = config
def run(self, instance: VastInstance) -> None:
"""Execute the benchmark script on the remote instance.
Args:
instance: Target instance for benchmark execution.
"""
benchmark_command = (
f"cd /app && "
f"TEST_ITERATIONS={self.config.test_iterations} "
f"CONTEXT_START={self.config.context_start} "
f"CONTEXT_END={self.config.context_end} "
f"CONTEXT_MULTIPLIER={self.config.context_multiplier} "
f"OLLAMA_MODEL={self.config.ollama_model} "
f"OLLAMA_FLASH_ATTENTION={self.config.ollama_flash_attention} "
f"OLLAMA_KEEP_ALIVE={self.config.ollama_keep_alive} "
f"OLLAMA_KV_CACHE_TYPE={self.config.ollama_kv_cache_type} "
f"OLLAMA_MAX_LOADED_MODELS={self.config.ollama_max_loaded_models} "
f"OLLAMA_NUM_GPU={self.config.ollama_num_gpu} "
f"OLLAMA_NUM_PARALLEL={self.config.ollama_num_parallel} "
f"OUTPUT_PATH={self.config.remote_results_path} "
f"uv run scripts/llm_benchmark.py"
)
logger.info("⚡ Starting benchmark execution...")
logger.debug("Benchmark command: %s", benchmark_command)
instance.execute_command_streaming(benchmark_command, timeout=1800) # 30 minutes
class RemoteBenchmarkOrchestrator:
"""Main orchestrator for remote GPU benchmarking workflows.
Coordinates the complete lifecycle of remote benchmarking from instance
provisioning through results retrieval and cleanup. Follows Single
Responsibility Principle by delegating specific tasks to specialised classes.
"""
def __init__(self, config: RemoteBenchmarkConfig) -> None:
"""Initialise orchestrator with configuration.
Args:
config: Complete benchmark configuration.
"""
self.config = config
self.client = VastAIDirect(api_key=config.api_key)
self.ollama_manager = OllamaManager(config)
self.benchmark_executor = BenchmarkExecutor(config)
def run(self) -> None:
"""Execute the complete remote benchmarking workflow.
Orchestrates all stages with proper error handling and cleanup.
Raises:
SystemExit: If any critical operation fails.
"""
logger.info("🚀 Starting remote benchmark process...")
# Prepare local environment
self.config.local_results_path.mkdir(parents=True, exist_ok=True)
# Launch and configure instance
instance = VastInstance(self.client, self.config)
try:
instance.launch()
# Setup Ollama environment and get version for results naming
ollama_version = self.ollama_manager.start_and_configure(instance)
self.config.set_ollama_version(ollama_version)
# Execute benchmark
self.benchmark_executor.run(instance)
# Retrieve results
instance.download_results(
self.config.remote_results_path, self.config.local_results_path
)
except VastAPIError as e:
# Handle known API errors gracefully without traceback spam
logger.error("Benchmark workflow failed: %s", str(e))
raise SystemExit(1) from e
except Exception as e:
# Unknown errors get full traceback for debugging
logger.exception("Benchmark workflow failed with unexpected error")
raise SystemExit(1) from e
finally:
# Always cleanup instance
instance.destroy()
logger.info("Process finished successfully.")
def main() -> None:
"""Main entry point for remote benchmarking orchestration.
Creates configuration from environment and executes the complete workflow.
Raises:
SystemExit: If any critical operation fails.
"""
try:
config = RemoteBenchmarkConfig.from_dotenv()
orchestrator = RemoteBenchmarkOrchestrator(config)
orchestrator.run()
except (FileNotFoundError, ValueError) as e:
logger.exception("Configuration error")
raise SystemExit(1) from e
except Exception as e:
logger.exception("Unexpected error in main")
raise SystemExit(1) from e
if __name__ == "__main__":
main()

852
uv.lock generated Normal file
View file

@ -0,0 +1,852 @@
version = 1
revision = 2
requires-python = ">=3.11"
[[package]]
name = "attrs"
version = "25.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
]
[[package]]
name = "bcrypt"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" },
{ url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" },
{ url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" },
{ url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" },
{ url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" },
{ url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" },
{ url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" },
{ url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" },
{ url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" },
{ url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" },
{ url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" },
{ url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" },
{ url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" },
{ url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" },
{ url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" },
{ url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" },
{ url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" },
{ url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" },
{ url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" },
{ url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" },
{ url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" },
{ url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" },
{ url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" },
{ url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" },
{ url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" },
{ url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" },
{ url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" },
{ url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" },
{ url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" },
{ url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" },
{ url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" },
{ url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" },
{ url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" },
{ url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" },
{ url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" },
{ url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" },
{ url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" },
{ url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" },
{ url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" },
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" },
{ url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103, upload-time = "2025-02-28T01:24:00.764Z" },
{ url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513, upload-time = "2025-02-28T01:24:02.243Z" },
{ url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685, upload-time = "2025-02-28T01:24:04.512Z" },
{ url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110, upload-time = "2025-02-28T01:24:05.896Z" },
]
[[package]]
name = "borb"
version = "2.0.32"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fonttools" },
{ name = "pillow" },
{ name = "python-barcode" },
{ name = "qrcode", extra = ["pil"] },
{ name = "requests" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/34/b12653ec6987bca846d06c0ee213f9099fa33d48b66f2552a7e7cde3f030/borb-2.0.32.tar.gz", hash = "sha256:601d7675151a9595d4786459d9c48893ef27cc4d5dc0616adcc86bd00ff434fe", size = 5563972, upload-time = "2022-08-07T21:41:29.416Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/5f/7692020c601edd0ef18ef99521f6de528c532230853e4f438555839c5bc2/borb-2.0.32-py3-none-any.whl", hash = "sha256:10cbfc4bb8cbeaa84992f37bc88f793b15ac31c2c96a3ab137c7f0e9353076a4", size = 6303676, upload-time = "2022-08-07T21:41:25.864Z" },
]
[[package]]
name = "certifi"
version = "2025.7.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
{ url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
{ url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
{ url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
{ url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
{ url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
{ url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
{ url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
{ url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[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 = "cryptography"
version = "45.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" },
{ url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" },
{ url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" },
{ url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" },
{ url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" },
{ url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" },
{ url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" },
{ url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" },
{ url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" },
{ url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" },
{ url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" },
{ url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" },
{ url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" },
{ url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" },
{ url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" },
{ url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" },
{ url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" },
{ url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" },
{ url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" },
{ url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" },
{ url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878, upload-time = "2025-07-02T13:06:06.339Z" },
{ url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" },
{ url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" },
{ url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" },
{ url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" },
{ url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" },
]
[[package]]
name = "fonttools"
version = "4.59.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387, upload-time = "2025-07-16T12:03:51.424Z" },
{ url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194, upload-time = "2025-07-16T12:03:53.295Z" },
{ url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333, upload-time = "2025-07-16T12:03:55.177Z" },
{ url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422, upload-time = "2025-07-16T12:03:57.406Z" },
{ url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631, upload-time = "2025-07-16T12:03:59.449Z" },
{ url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198, upload-time = "2025-07-16T12:04:01.542Z" },
{ url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216, upload-time = "2025-07-16T12:04:03.515Z" },
{ url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879, upload-time = "2025-07-16T12:04:05.015Z" },
{ url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562, upload-time = "2025-07-16T12:04:06.895Z" },
{ url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168, upload-time = "2025-07-16T12:04:08.695Z" },
{ url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850, upload-time = "2025-07-16T12:04:10.761Z" },
{ url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131, upload-time = "2025-07-16T12:04:12.846Z" },
{ url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667, upload-time = "2025-07-16T12:04:14.558Z" },
{ url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349, upload-time = "2025-07-16T12:04:16.388Z" },
{ url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315, upload-time = "2025-07-16T12:04:18.557Z" },
{ url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408, upload-time = "2025-07-16T12:04:20.489Z" },
{ url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704, upload-time = "2025-07-16T12:04:22.217Z" },
{ url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764, upload-time = "2025-07-16T12:04:23.985Z" },
{ url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699, upload-time = "2025-07-16T12:04:25.664Z" },
{ url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934, upload-time = "2025-07-16T12:04:27.733Z" },
{ url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319, upload-time = "2025-07-16T12:04:30.074Z" },
{ url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753, upload-time = "2025-07-16T12:04:32.292Z" },
{ url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688, upload-time = "2025-07-16T12:04:34.444Z" },
{ url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560, upload-time = "2025-07-16T12:04:36.034Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" },
]
[[package]]
name = "gpu-llm-benchmarking"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "paramiko" },
{ name = "python-dotenv" },
{ name = "requests" },
{ name = "vastai-sdk" },
]
[package.dev-dependencies]
dev = [
{ name = "mypy" },
{ name = "ruff" },
{ name = "types-paramiko" },
{ name = "types-requests" },
{ name = "uv" },
]
[package.metadata]
requires-dist = [
{ name = "paramiko", specifier = ">=3" },
{ name = "python-dotenv", specifier = ">=1" },
{ name = "requests", specifier = ">=2" },
{ name = "vastai-sdk", specifier = ">=0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "mypy", specifier = ">=1" },
{ name = "ruff", specifier = ">=0" },
{ name = "types-paramiko", specifier = ">=3" },
{ name = "types-requests", specifier = ">=2" },
{ name = "uv", specifier = ">=0" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "jsonschema"
version = "4.25.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" },
]
[[package]]
name = "jsonschema-specifications"
version = "2025.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" },
]
[[package]]
name = "mypy"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" },
{ url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" },
{ url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" },
{ url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" },
{ url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" },
{ url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" },
{ url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" },
{ url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" },
{ url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" },
{ url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" },
{ url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" },
{ url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" },
{ url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" },
{ url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" },
{ url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" },
{ url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" },
{ url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "paramiko"
version = "3.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bcrypt" },
{ name = "cryptography" },
{ name = "pynacl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/15/ad6ce226e8138315f2451c2aeea985bf35ee910afb477bae7477dc3a8f3b/paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822", size = 1566110, upload-time = "2025-02-04T02:37:59.783Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298, upload-time = "2025-02-04T02:37:57.672Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "pillow"
version = "11.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" },
{ url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" },
{ url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" },
{ url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" },
{ url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" },
{ url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" },
{ url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" },
{ url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" },
{ url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" },
{ url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" },
{ url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" },
{ url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" },
{ url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" },
{ url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" },
{ url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" },
{ url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" },
{ url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" },
{ url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" },
{ url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" },
{ url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
{ url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
{ url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
{ url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" },
{ url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" },
{ url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" },
{ url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
]
[[package]]
name = "pycparser"
version = "2.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
]
[[package]]
name = "pynacl"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" },
{ url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" },
{ url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" },
{ url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" },
{ url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" },
{ url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" },
{ url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" },
]
[[package]]
name = "pyparsing"
version = "3.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
]
[[package]]
name = "python-barcode"
version = "0.15.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/63/bc2fb47c9ba904b376780917f053b1c85b87085fd44948590e71c11187b0/python-barcode-0.15.1.tar.gz", hash = "sha256:3b1825fbdb11e597466dff4286b4ea9b1e86a57717b59e563ae679726fc854de", size = 228161, upload-time = "2023-07-05T22:56:59.962Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/27/9b5c5bb1938d4e6b12f4c95f40ea905c11df3cd58e128e9305397b9a2697/python_barcode-0.15.1-py3-none-any.whl", hash = "sha256:057636fba37369c22852410c8535b36adfbeb965ddfd4e5b6924455d692e0886", size = 212956, upload-time = "2023-07-05T22:56:58.596Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-dotenv"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "qrcode"
version = "8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" },
]
[package.optional-dependencies]
pil = [
{ name = "pillow" },
]
[[package]]
name = "referencing"
version = "0.36.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "rpds-py"
version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610, upload-time = "2025-07-01T15:53:58.844Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032, upload-time = "2025-07-01T15:53:59.985Z" },
{ url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525, upload-time = "2025-07-01T15:54:01.162Z" },
{ url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089, upload-time = "2025-07-01T15:54:02.319Z" },
{ url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255, upload-time = "2025-07-01T15:54:03.38Z" },
{ url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283, upload-time = "2025-07-01T15:54:04.923Z" },
{ url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881, upload-time = "2025-07-01T15:54:06.482Z" },
{ url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822, upload-time = "2025-07-01T15:54:07.605Z" },
{ url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347, upload-time = "2025-07-01T15:54:08.591Z" },
{ url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956, upload-time = "2025-07-01T15:54:09.963Z" },
{ url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363, upload-time = "2025-07-01T15:54:11.073Z" },
{ url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123, upload-time = "2025-07-01T15:54:12.382Z" },
{ url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732, upload-time = "2025-07-01T15:54:13.434Z" },
{ url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917, upload-time = "2025-07-01T15:54:14.559Z" },
{ url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload-time = "2025-07-01T15:54:15.734Z" },
{ url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload-time = "2025-07-01T15:54:16.922Z" },
{ url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload-time = "2025-07-01T15:54:18.101Z" },
{ url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload-time = "2025-07-01T15:54:19.295Z" },
{ url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload-time = "2025-07-01T15:54:20.858Z" },
{ url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload-time = "2025-07-01T15:54:22.508Z" },
{ url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload-time = "2025-07-01T15:54:23.987Z" },
{ url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload-time = "2025-07-01T15:54:25.073Z" },
{ url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload-time = "2025-07-01T15:54:26.225Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload-time = "2025-07-01T15:54:27.424Z" },
{ url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload-time = "2025-07-01T15:54:29.93Z" },
{ url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload-time = "2025-07-01T15:54:31.128Z" },
{ url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload-time = "2025-07-01T15:54:32.195Z" },
{ url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload-time = "2025-07-01T15:54:33.271Z" },
{ url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" },
{ url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" },
{ url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" },
{ url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" },
{ url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" },
{ url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" },
{ url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" },
{ url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" },
{ url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" },
{ url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" },
{ url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" },
{ url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" },
{ url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" },
{ url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" },
{ url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" },
{ url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" },
{ url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" },
{ url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" },
{ url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" },
{ url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" },
{ url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" },
{ url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" },
{ url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload-time = "2025-07-01T15:55:17.001Z" },
{ url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload-time = "2025-07-01T15:55:18.922Z" },
{ url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875, upload-time = "2025-07-01T15:55:20.399Z" },
{ url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993, upload-time = "2025-07-01T15:55:21.729Z" },
{ url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683, upload-time = "2025-07-01T15:55:22.918Z" },
{ url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825, upload-time = "2025-07-01T15:55:24.207Z" },
{ url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292, upload-time = "2025-07-01T15:55:25.554Z" },
{ url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435, upload-time = "2025-07-01T15:55:27.798Z" },
{ url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410, upload-time = "2025-07-01T15:55:29.057Z" },
{ url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724, upload-time = "2025-07-01T15:55:30.719Z" },
{ url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285, upload-time = "2025-07-01T15:55:31.981Z" },
{ url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459, upload-time = "2025-07-01T15:55:33.312Z" },
{ url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083, upload-time = "2025-07-01T15:55:34.933Z" },
{ url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291, upload-time = "2025-07-01T15:55:36.202Z" },
{ url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload-time = "2025-07-01T15:55:37.483Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload-time = "2025-07-01T15:55:38.828Z" },
{ url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330, upload-time = "2025-07-01T15:55:40.175Z" },
{ url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254, upload-time = "2025-07-01T15:55:42.015Z" },
{ url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094, upload-time = "2025-07-01T15:55:43.603Z" },
{ url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889, upload-time = "2025-07-01T15:55:45.275Z" },
{ url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301, upload-time = "2025-07-01T15:55:47.098Z" },
{ url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891, upload-time = "2025-07-01T15:55:48.412Z" },
{ url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044, upload-time = "2025-07-01T15:55:49.816Z" },
{ url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774, upload-time = "2025-07-01T15:55:51.192Z" },
{ url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload-time = "2025-07-01T15:55:52.541Z" },
{ url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload-time = "2025-07-01T15:55:53.874Z" },
{ url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" },
{ url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505, upload-time = "2025-07-01T15:56:34.716Z" },
{ url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468, upload-time = "2025-07-01T15:56:36.219Z" },
{ url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680, upload-time = "2025-07-01T15:56:37.644Z" },
{ url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035, upload-time = "2025-07-01T15:56:39.241Z" },
{ url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922, upload-time = "2025-07-01T15:56:40.645Z" },
{ url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822, upload-time = "2025-07-01T15:56:42.137Z" },
{ url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336, upload-time = "2025-07-01T15:56:44.239Z" },
{ url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871, upload-time = "2025-07-01T15:56:46.284Z" },
{ url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439, upload-time = "2025-07-01T15:56:48.549Z" },
{ url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380, upload-time = "2025-07-01T15:56:50.086Z" },
{ url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334, upload-time = "2025-07-01T15:56:51.703Z" },
]
[[package]]
name = "ruff"
version = "0.12.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" },
{ url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" },
{ url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" },
{ url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" },
{ url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" },
{ url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" },
{ url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" },
{ url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" },
{ url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" },
{ url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" },
{ url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" },
{ url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" },
{ url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" },
{ url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" },
{ url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" },
]
[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "types-paramiko"
version = "3.5.0.20250708"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/7e/a50c3538a088eb3dbf167d16f5b0d8b62c821496df7488af4d7841c06bc5/types_paramiko-3.5.0.20250708.tar.gz", hash = "sha256:45125bff40766db2d7d53bd1a4f329868293e512f2e6b7fe11c335111df027f2", size = 28877, upload-time = "2025-07-08T03:15:04.769Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/42/0a052da12277848f44825532d97858d2ba8851b603b0a4923356f503e31c/types_paramiko-3.5.0.20250708-py3-none-any.whl", hash = "sha256:c761ecff3445d847a40c0d831a0daaba2bddbd2bdc74315659be5ff4835bb434", size = 39758, upload-time = "2025-07-08T03:15:03.95Z" },
]
[[package]]
name = "types-requests"
version = "2.32.4.20250611"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "uv"
version = "0.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/2c3cb3e992fa1bf9af590bb37983f13e3ae67155820a09a98945664f71f3/uv-0.8.3.tar.gz", hash = "sha256:2ccaae4c749126c99f6404d67a0ae1eae29cbafb05603d09094a775061fdf4e5", size = 3415565, upload-time = "2025-07-24T21:14:34.417Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/ab/7b881bb236b9c5f6d99a98adf0c4d1e7c4f0cf4b49051d6d24eb82f19c10/uv-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:ae7efe91dcfc24126fa91e0fb69a1daf6c0e494a781ba192bb0cc62d7ab623ee", size = 17912668, upload-time = "2025-07-24T21:13:50.682Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9b/64d2ed7388ce88971ffb93d45e74465c95bb885bff40c93f5037b7250930/uv-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:966ec7d7f57521fef0fee685d71e183c9cafb358ddcfe27519dfeaf40550f247", size = 17947557, upload-time = "2025-07-24T21:13:54.59Z" },
{ url = "https://files.pythonhosted.org/packages/9c/ba/8ceec5d6a1adf6b827db557077d8059e573a84c3708a70433d22a0470fab/uv-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f904f574dc2d7aa1d96ddf2483480ecd121dc9d060108cadd8bff100b754b64", size = 16638472, upload-time = "2025-07-24T21:13:57.57Z" },
{ url = "https://files.pythonhosted.org/packages/a3/76/6d2eb90936603756c4a71f9cf5de8d9214fa4d11dcb5a89117389acecd5e/uv-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8b16f1bddfdf8f7470924ab34a7b55e4c372d5340c7c1e47e7fc84a743dc541f", size = 17221472, upload-time = "2025-07-24T21:14:00.158Z" },
{ url = "https://files.pythonhosted.org/packages/5b/bf/c3e1cc9604b114dfb49a3a40a230b5410fc97776c149ca73bb524990f9ba/uv-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:526f2c3bd6f311ce31f6f7b6b7d818b191f41e76bed3aaab671b716220c02d8f", size = 17607299, upload-time = "2025-07-24T21:14:02.226Z" },
{ url = "https://files.pythonhosted.org/packages/53/16/819f876f5ca2f8989c19d9b65b7d794d60e6cca0d13187bbc8c8b5532b52/uv-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76de331a07e5ae9b6490e70a9439a072b91b3167a5684510af10c2752c4ece9a", size = 18218124, upload-time = "2025-07-24T21:14:04.809Z" },
{ url = "https://files.pythonhosted.org/packages/61/a8/1df852a9153fec0c713358a50cfd7a21a4e17b5ed5704a390c0f3da448ab/uv-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:989898caeb6e972979543b57547d1c28ab8af81ff8fc15921fd354c17d432749", size = 19638846, upload-time = "2025-07-24T21:14:07.074Z" },
{ url = "https://files.pythonhosted.org/packages/ac/31/adeedaa009d8d919107c52afb58689d5e9db578b07f8dea5e15e4c738d52/uv-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce7981f4fbeecf93dc5cf0a5a7915e84956fd99ad3ac977c048fe0cfdb1a17e", size = 19384261, upload-time = "2025-07-24T21:14:09.425Z" },
{ url = "https://files.pythonhosted.org/packages/8d/87/b3981f499e2b13c5ef0022fd7809f0fccbecd41282ae4f6a0e3fd5fa1430/uv-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8486f7576d15cc73509f93f47b3190f44701ea36839906369301b58c8604d5db", size = 18673722, upload-time = "2025-07-24T21:14:11.656Z" },
{ url = "https://files.pythonhosted.org/packages/5e/62/0d1ba1c666c5492d3716d8d3fba425f65ed2acc6707544c3cbbd381f6cbe/uv-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1eb7c896fc0d80ed534748aaf46697b6ebc8ce401f1c51666ce0b9923c3db9a", size = 18658829, upload-time = "2025-07-24T21:14:13.798Z" },
{ url = "https://files.pythonhosted.org/packages/cc/ae/11d09be3c74ca4896d55701ebbca7fe7a32db0502cf9f4c57e20bf77bfc4/uv-0.8.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1121ad1c9389b865d029385031d3fd7d90d343c92a2149a4d4aa20bf469cb27f", size = 17460029, upload-time = "2025-07-24T21:14:15.993Z" },
{ url = "https://files.pythonhosted.org/packages/22/47/b67296c62381b8369f082a33d9fdcb7c579ad9922bcce7b09cd4af935dfa/uv-0.8.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5313ee776ad65731ffa8ac585246f987d3a2bf72e6153c12add1fff22ad6e500", size = 18398665, upload-time = "2025-07-24T21:14:18.399Z" },
{ url = "https://files.pythonhosted.org/packages/01/5f/23990de5487085ca86e12f99d0a8f8410419442ffd35c42838675df5549b/uv-0.8.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:daa6e0d657a94f20e962d4a03d833ef7af5c8e51b7c8a2d92ba6cf64a4c07ac1", size = 17560408, upload-time = "2025-07-24T21:14:20.609Z" },
{ url = "https://files.pythonhosted.org/packages/89/42/1a8ce79d2ce7268e52690cd0f1b6c3e6c8d748a68d42de206e37219e9627/uv-0.8.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ad13453ab0a1dfa64a221aac8f52199efdcaa52c97134fffd7bcebed794a6f4b", size = 17758504, upload-time = "2025-07-24T21:14:23.086Z" },
{ url = "https://files.pythonhosted.org/packages/6b/39/ae94e06ac00cb5002e636af0e48c5180fab5b50a463dc96386875ea511ea/uv-0.8.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:5843cc43bafad05cc710d8e31bd347ee37202462a63d32c30746e9df48cfbda2", size = 18741736, upload-time = "2025-07-24T21:14:25.329Z" },
{ url = "https://files.pythonhosted.org/packages/18/e0/a2fe9cc5f7b8815cbf97cb1bf64abb71fcb65f25ca7a5a8cdd4c2e23af97/uv-0.8.3-py3-none-win32.whl", hash = "sha256:17bcdb0615e37cc5f985f7d7546f755ac6343c1dc8bbe876c892437f14f8f904", size = 17723422, upload-time = "2025-07-24T21:14:28.02Z" },
{ url = "https://files.pythonhosted.org/packages/cf/c3/da508ec0f6883f1c269a0a477bb6447c81d5383fe3ad5d5ea3d45469fd30/uv-0.8.3-py3-none-win_amd64.whl", hash = "sha256:2e311c029bff2ca07c6ddf877ccc5935cabb78e09b94b53a849542665b6a6fa1", size = 19531666, upload-time = "2025-07-24T21:14:30.192Z" },
{ url = "https://files.pythonhosted.org/packages/b2/8d/c0354e416697b4baa7ceaad0e423639b6683d1f8299355e390a64809f7bf/uv-0.8.3-py3-none-win_arm64.whl", hash = "sha256:391c97577048a40fd8c85b370055df6420f26e81df7fa906f0e0ce1aa2af3527", size = 18161557, upload-time = "2025-07-24T21:14:32.482Z" },
]
[[package]]
name = "vastai-sdk"
version = "0.1.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "borb" },
{ name = "jsonschema" },
{ name = "pyparsing" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "requests" },
{ name = "urllib3" },
{ name = "xdg" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/01/58bbfc7a45cfe2068a374ab91e396d4b3a51030f8bd1543c3377220900e5/vastai_sdk-0.1.19.tar.gz", hash = "sha256:a9871f316a860abdcabcafb00bd47a31d0d61b3bcdca52b9f13563eb110651d0", size = 67211, upload-time = "2025-04-30T17:27:22.877Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/d0/e710a6512b0c950a8c35aef7fc67767ee39902e2c02a553d74c9b35e527b/vastai_sdk-0.1.19-py3-none-any.whl", hash = "sha256:a81cf057123e5fe19c7dfa22199a311d353f73d109559185c9ef0b232b37aba7", size = 65550, upload-time = "2025-04-30T17:27:21.754Z" },
]
[[package]]
name = "xdg"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/b9/0e6e6f19fb75cf5e1758f4f33c1256738f718966700cffc0fde2f966218b/xdg-6.0.0.tar.gz", hash = "sha256:24278094f2d45e846d1eb28a2ebb92d7b67fc0cab5249ee3ce88c95f649a1c92", size = 3453, upload-time = "2023-02-27T19:27:44.309Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/54/3516c1cf349060fc3578686d271eba242f10ec00b4530c2985af9faac49b/xdg-6.0.0-py3-none-any.whl", hash = "sha256:df3510755b4395157fc04fc3b02467c777f3b3ca383257397f09ab0d4c16f936", size = 3855, upload-time = "2023-02-27T19:27:42.151Z" },
]