Initial commit
All checks were successful
Build Vast.ai Ollama Benchmark Image / Build and Push (push) Successful in 4m43s
All checks were successful
Build Vast.ai Ollama Benchmark Image / Build and Push (push) Successful in 4m43s
This commit is contained in:
commit
86e9de9e75
25 changed files with 4367 additions and 0 deletions
26
.env.example
Normal file
26
.env.example
Normal 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
|
50
.forgejo/workflows/build_vast.ai_image.yaml
Normal file
50
.forgejo/workflows/build_vast.ai_image.yaml
Normal 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
57
.github/workflows/docker-build.yml
vendored
Normal 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
49
.gitignore
vendored
Normal 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
31
Dockerfile.ollama
Normal 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
73
LICENSE
Normal 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
185
README.md
Normal 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
215
docs/python-sdk-usage.md
Normal 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
111
pyproject.toml
Normal 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
77
results/README.md
Normal 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
58
scripts/README.md
Normal 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.
|
7
scripts/helpers/__init__.py
Normal file
7
scripts/helpers/__init__.py
Normal 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
|
271
scripts/helpers/interactive.py
Normal file
271
scripts/helpers/interactive.py
Normal 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
79
scripts/helpers/logger.py
Normal 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
245
scripts/helpers/models.py
Normal 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
|
201
scripts/helpers/nvidia_gpu_monitor.py
Normal file
201
scripts/helpers/nvidia_gpu_monitor.py
Normal 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,
|
||||
)
|
39
scripts/helpers/ollama_client.py
Normal file
39
scripts/helpers/ollama_client.py
Normal 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 {}
|
153
scripts/helpers/ollama_manager.py
Normal file
153
scripts/helpers/ollama_manager.py
Normal 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)
|
50
scripts/helpers/prompt_generator.py
Normal file
50
scripts/helpers/prompt_generator.py
Normal 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)
|
288
scripts/helpers/results_manager.py
Normal file
288
scripts/helpers/results_manager.py
Normal 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")
|
233
scripts/helpers/vastai_api.py
Normal file
233
scripts/helpers/vastai_api.py
Normal 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
|
518
scripts/helpers/vastai_instance.py
Normal file
518
scripts/helpers/vastai_instance.py
Normal 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
339
scripts/llm_benchmark.py
Normal 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()
|
160
scripts/run_vast.ai_benchmark.py
Normal file
160
scripts/run_vast.ai_benchmark.py
Normal 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
852
uv.lock
generated
Normal 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" },
|
||||
]
|
Loading…
Add table
Add a link
Reference in a new issue