diff --git a/.env-template.yaml b/.env-template.yaml deleted file mode 100644 index a074b39..0000000 --- a/.env-template.yaml +++ /dev/null @@ -1,9 +0,0 @@ -account_id: "value" - -read: - access_key_id: "value" - secret_access_key: "value" - -write: - access_key_id: "value" - secret_access_key: "value" diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8f350e1 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +WANDB_API_KEY=your_api_key_here \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b3481d..472a798 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,16 +22,6 @@ jobs: - name: Install dependencies run: uv sync --extra all - - name: Create .env.yaml - run: | - echo "account_id: \"$ACCOUNT_ID\"" > .env.yaml - echo "read:" >> .env.yaml - echo " access_key_id: \"$READ_ACCESS_KEY_ID\"" >> .env.yaml - echo " secret_access_key: \"$READ_SECRET_ACCESS_KEY\"" >> .env.yaml - echo "write:" >> .env.yaml - echo " access_key_id: \"$WRITE_ACCESS_KEY_ID\"" >> .env.yaml - echo " secret_access_key: \"$WRITE_SECRET_ACCESS_KEY\"" >> .env.yaml - - name: Run Ruff Lint uses: astral-sh/ruff-action@v1 with: @@ -40,7 +30,4 @@ jobs: - name: Run Ruff Format uses: astral-sh/ruff-action@v1 with: - args: format --check - - - name: Run Tests - run: uv run pytest \ No newline at end of file + args: format --check \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..8eef53f --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,60 @@ +name: Docker Build and Publish + +on: + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: tplr-ai/templar + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Release tags (v1.0.0, 1.0.0) + type=semver,pattern={{version}} + type=semver,pattern={{raw}} + # Latest tag on release or main branch + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + # SHA for every build + type=sha,prefix=sha-,format=short + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.gitignore b/.gitignore index 77c70eb..29d513a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ venv/ .env/ uv.lock +# Ruff sepcific +.ruff* + # IDE .idea/ .vscode/ @@ -40,14 +43,4 @@ uv.lock # Project specific wandb -checkpoint-* -checkpoints.ipynb -analysis.ipynb -scratch* -bad_slices/ -good_slices -slices -foo* -# Evals -lm-evaluation-harness -models \ No newline at end of file +*ipynb \ No newline at end of file diff --git a/README.md b/README.md index 099c936..c7b61ae 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,308 @@ ___ _ _ _ _ | _ _ | | +
-Documentation: Incentive DesignMinerValidator +Documentation: MinerValidator
- +### Formal Definitions + +- **Model Parameters** at time \( t \): \( \theta^t \). +- **Local Gradients**: \( g_i^t \), computed by miner \( i \) at time \( t \). +- **Momentum Buffer Update**: + \[ + m_i^{t+1} = \gamma m_i^t + \eta g_i^t + \] +- **Weight Decay**: + \[ + \theta^{t+1} = (1 - \lambda)\theta^t + \] +- **Compressed Gradient**: \( \tilde{g}_i^t \), the top-k compressed version of the transformed momentum buffer. +- **Aggregated Gradient**: + \[ + \delta_{\text{agg}} = \sum_{i \in \mathcal{P}} \tilde{g}_i^t + \] + - \( \mathcal{P} \): Set of peer miners. + +--- + +## Validators + +### Operations + +1. **Model Synchronization**: + - Synchronize their model with the latest global state. + - Attempt to load the latest model checkpoint from the validator with the highest stake or start from scratch. + +2. **Data Acquisition**: + - Select a miner to evaluate. + - Retrieve the same data subset assigned to that miner using the same deterministic seeding mechanism. + +3. **Gradient Gathering**: + - Gather the compressed gradients submitted by miners for the current window. + - Decompress and apply these gradients to their local model to maintain consistency. + +4. **Evaluation of Miners**: + - For the selected miner \( i \): + - **Compute Loss Before** applying the miner's gradient: + \[ + L_{\text{before}} = \mathcal{L}(\theta^t; D_i) + \] + - \( D_i \): Dataset assigned to miner \( i \). + - **Apply** the miner's gradient: + \[ + \theta^{t+1} = \theta^t + \delta_i + \] + - \( \delta_i \): Decompressed gradient from miner \( i \). + - **Compute Loss After** applying the gradient: + \[ + L_{\text{after}} = \mathcal{L}(\theta^{t+1}; D_i) + \] + - **Compute Improvement**: + \[ + s_i = L_{\text{before}} - L_{\text{after}} + \] + +5. **Score Calculation**: + - The score \( s_i \) reflects the miner's contribution to reducing the loss. + +6. **Weight Assignment and Update**: + - Update the moving average of the miner's score: + \[ + \bar{s}_i = \alpha s_i + (1 - \alpha)\bar{s}_i + \] + - \( \alpha \): Smoothing factor. + - Compute weights using a softmax function over the moving average scores: + \[ + w_i = \frac{e^{\bar{s}_i}}{\sum_{j \in \mathcal{M}} e^{\bar{s}_j}} + \] + - \( \mathcal{M} \): Set of miners. + +7. **Blockchain Update**: + - Validators set these weights on the blockchain, influencing reward distribution and miner reputation. + +8. **Optimizer Step and Learning Rate Scheduling**: + - Apply optimizer steps and adjust learning rates to keep the model updated. + +--- + +### Formal Definitions + +- **Model Loss**: + - Before update: \( L_{\text{before}} = \mathcal{L}(\theta^t; D_i) \) + - After update: \( L_{\text{after}} = \mathcal{L}(\theta^{t+1}; D_i) \) +- **Miner's Score**: + \[ + s_i = L_{\text{before}} - L_{\text{after}} + \] +- **Moving Average Score**: + \[ + \bar{s}_i = \alpha s_i + (1 - \alpha)\bar{s}_i + \] +- **Assigned Weight**: + \[ + w_i = \frac{e^{\bar{s}_i}}{\sum_{j} e^{\bar{s}_j}} + \] + +--- + +## Incentive Mechanism + +### Objective + +The incentive mechanism in **τemplar** aims to: + +- **Encourage Honest Participation**: Motivate miners to perform genuine training and provide updates that improve model performance. +- **Promote Model Improvement**: Reward updates that lead to a reduction in loss. +- **Discourage Malicious Behavior**: Penalize updates that do not improve or degrade model performance. + +### Detailed Explanation + +#### Score Calculation and Weight Assignment + +1. **Compute Loss Improvement**: + - Validators measure the effectiveness of a miner's update by the reduction in loss on the assigned dataset. + - The score \( s_i = L_{\text{before}} - L_{\text{after}} \) quantifies this improvement. + +2. **Interpretation of Scores**: + - **Positive \( s_i \)**: Indicates the miner's update improved the model. + - **Zero \( s_i \)**: No change in model performance. + - **Negative \( s_i \)**: The miner's update worsened the model. + +3. **Moving Average for Stability**: + - Using a moving average \( \bar{s}_i \) smooths out fluctuations in individual scores. + - Helps in maintaining stable weights over time. + +4. **Weight Computation**: + - Apply the softmax function to the moving average scores to compute the weights: + \[ + w_i = \frac{e^{\bar{s}_i}}{\sum_{j} e^{\bar{s}_j}} + \] + - Ensures that miners with higher contributions receive proportionally higher weights. + +5. **Blockchain Update**: + - Validators set these weights on the blockchain, which influences reward distribution and miner reputation. + +### Formal Guarantees + +1. **Alignment Incentive**: + - Miners are incentivized to produce updates that reduce the loss, aligning individual goals with the collective objective. + +2. **Discouraging Malicious Actions**: + - Miners submitting harmful updates receive lower or negative scores, resulting in minimal or no rewards. + +3. **Fair Reward Distribution**: + - Weights are computed based on the actual performance improvements contributed by miners. + +4. **Convergence Assurance**: + - By aggregating beneficial updates, the model is guided towards convergence and improved performance. + +5. **Sybil Resistance**: + - Since rewards are based on contribution quality, creating fake identities without meaningful contributions offers no advantage. + +--- + +## Formal Analysis + +### Miner Utility Maximization + +Miners aim to maximize their expected reward, which is proportional to their assigned weight \( w_i \): + +\[ +\max_{\delta_i} \quad w_i = \frac{e^{\bar{s}_i}}{\sum_{j} e^{\bar{s}_j}} +\] + +Subject to: + +- **Update Rule**: + \[ + \delta_i = \text{Compress}(\gamma m_i^{t} + \eta g_i^t) + \] +- **Model Update**: + \[ + \theta^{t+1} = \theta^{t} + \delta_{\text{agg}} + \] +- **Score Function**: + \[ + s_i = L(\theta^{t}; D_i) - L(\theta^{t} + \delta_i; D_i) + \] + +The optimal strategy for miners is to compute accurate gradients that lead to a reduction in loss on their assigned data. + +### Validator Consistency + +Validators ensure: + +- **Fair Evaluation**: + - Use the same datasets as miners to compute losses. + - Apply the miners' updates accurately. + +- **Transparency**: + - Evaluation procedures are deterministic and replicable. + +### Security Considerations + +1. **Data Integrity**: + - Deterministic data assignments prevent miners from manipulating their datasets. + +2. **Gradient Compression and Privacy**: + - Compression reduces the risk of exposing sensitive information. + - Only significant components are shared. + +3. **Preventing Free Riding**: + - Miners gain rewards only if their updates lead to performance improvements. + +--- + +## Conclusion + +The incentive mechanism in **τemplar** effectively encourages miners to contribute meaningful updates that enhance the global model. By tying rewards to the actual improvement in model loss resulting from miners' gradients, the system ensures that only beneficial contributions are rewarded. This approach aligns individual miner incentives with the collective goal of training an effective model. + +The careful design of data assignment, gradient evaluation, and weight distribution fosters a self-regulating ecosystem where honest participation is the most profitable strategy. Formal guarantees provide robustness against malicious actors, promoting the overall improvement of the model through collaborative effort. + +Thus, **τemplar** creates a sustainable and efficient framework for decentralized collaborative learning, leveraging incentives to drive positive contributions and advance the shared model's performance. + diff --git a/assets/acl_perms.png b/assets/acl_perms.png deleted file mode 100644 index 9c9a87e..0000000 Binary files a/assets/acl_perms.png and /dev/null differ diff --git a/assets/allow_public_access.png b/assets/allow_public_access.png deleted file mode 100644 index 063baca..0000000 Binary files a/assets/allow_public_access.png and /dev/null differ diff --git a/assets/object_owner_preferred.png b/assets/object_owner_preferred.png deleted file mode 100644 index a386664..0000000 Binary files a/assets/object_owner_preferred.png and /dev/null differ diff --git a/blacklist.toml b/blacklist.toml deleted file mode 100644 index e37cc5b..0000000 --- a/blacklist.toml +++ /dev/null @@ -1,28 +0,0 @@ -[blacklist] -hotkeys = [ - "5F1AvW1YMA3wycLPDfvC6FgEeu4cXaicZ3nxsu51WQsu7ZzY", - "5CkrU6o7eDNbf3SJdyzZu4fK96eHr4KFhjUjFRtbsgZHEXAC", - "5G7AawrGnYhPgS8nHC9W93MsWan3R3i8VJ3qHaetMBDRf9zx", - "5GTRujZS7JoCHEYapRD32o2iK26z9nsM7u1ZEmnc1BmGQTLb", - "5EAJxoyP1QrUkCyyDf5zx89uVtuZ2cxJjoA1eixN1gwT4xvT", - "5F28AhQcDSoMPrnoAgQc1TmvBTBS5HPXLovuA3dgXqJK9kRn", - "5CdTAxpZcnCJP5MZ7qNncgbFSddbyKf9dyzM5WSbkXYkDmAQ", - "5ERxpTnQBHQSFSHxQfBwncyg2nX6fwd9zuAgWEc9zhKSS3o4", - "5ENnzZUHbMqjeZw4nMffCfaRBVRX3wPcN4qNd9g1dSzdQ3AS", - "5CVD4y13Lo1k9mN46d39i8StB5ecMz4sA3YqMaHjUKg5MNE1", - "5EHd7FxG2sPqTNbmEjDU9SiMkPGdbiPNw2PXgf6QBPjG3Sge", - "5CMk8v2x8DSeKhggxTTwdXpiTDaogFdFh34vrvawgCn62Ri3", - "5E9t5M8YaetNoXbwdb16zUfhrtJGLTjqUZLSxtSAjBquMYk8", - "5EFPzb95KBbKZWFraUqLgf93uatCKz11eGKfF1mHN6zN5hix", - "5HgBkh5nYPvJwfWwyZdPTZepx1Ta3s7VvqqyK4ycmB38MASW", - "5CP6EzFLcWZZ5CCZGCi96EhbgxMTQ3Ragq1ziGyXXjMEywpX", - "5DfeE7AzhPtcSQ7HfkLuBb3o3EHRsHwMkirkW1DHeS2576zD", - "5EUwLvCGQGWVBzuKCmVig3z2WnPScmToPvXPd4AJKBKJ9oAB", - "5CfjjV4xaxJ1Bg82ysU9TFEwu4Bt85skz3MKhAdUyKKmqoNp", - "5Gbj4KaQPgDMdVgaaMXrzNqSYQPBXz7mCT7XfESLyLW7F3pP", - "5DHXoVMi6VWigqdeCwGuxUKPJuCcJrRdTnbN85T4vgEdTcn9", - "5GgX9NvYVcp1to4kyUDXyxyc8zEVBK51s4m8geEjfrzpmbzt", - "5FcPNdTGmVMAmimvkUkFACfZQprhrPSzcwx1sQNUCfDghxe2", - "5F1K7TEV5a7bK2bt7zT3K7wLPWrq4QcBeq9ooTWWWLuVyWbo", - "5Ev5oSnyb2QazBzXG5WdzonB6v2wKMLM6DTps7sLRuHC84Mp" -] \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..40c4eb7 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,44 @@ +# Use NVIDIA CUDA base image +FROM nvidia/cuda:12.6.0-runtime-ubuntu22.04 + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ + python3-venv \ + git \ + curl \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install uv +RUN pip install uv + +ENV PATH="/root/.cargo/bin:${PATH}" + +# Copy pyproject.toml first for better caching +COPY pyproject.toml . +COPY . . + +# Install dependencies using uv +RUN uv sync + +# Environment variables that can be passed +ENV WALLET_NAME="" +ENV WALLET_HOTKEY="" +ENV CUDA_DEVICE="cuda:0" +ENV NETWORK="test" +ENV DEBUG="false" +ENV NODE_TYPE="" + +# Make entrypoint script executable +RUN chmod +x /app/scripts/entrypoint.sh + +# Create volumes for persistence +VOLUME ["/root/.bittensor/wallets", "/app/logs"] + +# Set entrypoint +ENTRYPOINT ["/app/scripts/entrypoint.sh"] \ No newline at end of file diff --git a/docker/compose.yml b/docker/compose.yml new file mode 100644 index 0000000..e32b9bd --- /dev/null +++ b/docker/compose.yml @@ -0,0 +1,46 @@ +services: + node: + image: ghcr.io/tplr-ai/templar:latest + container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} + restart: unless-stopped + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ./logs:/app/logs + environment: + - NODE_TYPE=${NODE_TYPE:-miner} + - WALLET_NAME=${WALLET_NAME} + - WALLET_HOTKEY=${WALLET_HOTKEY} + - CUDA_DEVICE=${CUDA_DEVICE:-cuda:0} + - NETWORK=${NETWORK:-finney} + - DEBUG=${DEBUG:-false} + - NETUID=${NETUID:-268} + - WANDB_API_KEY=${WANDB_API_KEY} + - HOST_CUDA_VERSION=12.6 + - R2_ACCOUNT_ID=${R2_ACCOUNT_ID} + - R2_READ_ACCESS_KEY_ID=${R2_READ_ACCESS_KEY_ID} + - R2_READ_SECRET_ACCESS_KEY=${R2_READ_SECRET_ACCESS_KEY} + - R2_WRITE_ACCESS_KEY_ID=${R2_WRITE_ACCESS_KEY_ID} + - R2_WRITE_SECRET_ACCESS_KEY=${R2_WRITE_SECRET_ACCESS_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] + labels: + - "com.centurylinklabs.watchtower.enable=true" + + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${HOME}/.docker/config.json:/config.json:ro + command: --interval 30 --cleanup --label-enable + restart: unless-stopped + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_LABEL_ENABLE=true + - WATCHTOWER_INCLUDE_RESTARTING=true + - REPO_USER=${GITHUB_USER} + - REPO_PASSWORD=${GITHUB_TOKEN} diff --git a/docker/docker-compose-test.yml b/docker/docker-compose-test.yml new file mode 100644 index 0000000..84eb20f --- /dev/null +++ b/docker/docker-compose-test.yml @@ -0,0 +1,93 @@ +services: + miner1: + build: + context: .. + dockerfile: docker/Dockerfile + container_name: templar-miner-M111 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ../logs:/app/logs + environment: + NODE_TYPE: miner + WALLET_NAME: Bistro + WALLET_HOTKEY: M111 + CUDA_DEVICE: cuda:0 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} + NETUID: 268 + HOST_CUDA_VERSION: 12.6 + R2_ACCOUNT_ID: ${R2_ACCOUNT_ID} + R2_READ_ACCESS_KEY_ID: ${R2_READ_ACCESS_KEY_ID} + R2_READ_SECRET_ACCESS_KEY: ${R2_READ_SECRET_ACCESS_KEY} + R2_WRITE_ACCESS_KEY_ID: ${R2_WRITE_ACCESS_KEY_ID} + R2_WRITE_SECRET_ACCESS_KEY: ${R2_WRITE_SECRET_ACCESS_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] + + miner2: + build: + context: .. + dockerfile: docker/Dockerfile + container_name: templar-miner-M222 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ../logs:/app/logs + environment: + NODE_TYPE: miner + WALLET_NAME: Bistro + WALLET_HOTKEY: M222 + CUDA_DEVICE: cuda:1 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} + NETUID: 268 + HOST_CUDA_VERSION: 12.6 + R2_ACCOUNT_ID: ${R2_ACCOUNT_ID} + R2_READ_ACCESS_KEY_ID: ${R2_READ_ACCESS_KEY_ID} + R2_READ_SECRET_ACCESS_KEY: ${R2_READ_SECRET_ACCESS_KEY} + R2_WRITE_ACCESS_KEY_ID: ${R2_WRITE_ACCESS_KEY_ID} + R2_WRITE_SECRET_ACCESS_KEY: ${R2_WRITE_SECRET_ACCESS_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] + + validator: + build: + context: .. + dockerfile: docker/Dockerfile + container_name: templar-validator-V11 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ../logs:/app/logs + environment: + NODE_TYPE: validator + WALLET_NAME: Bistro + WALLET_HOTKEY: V11 + CUDA_DEVICE: cuda:2 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} + NETUID: 268 + HOST_CUDA_VERSION : 12.6 + R2_ACCOUNT_ID: ${R2_ACCOUNT_ID} + R2_READ_ACCESS_KEY_ID: ${R2_READ_ACCESS_KEY_ID} + R2_READ_SECRET_ACCESS_KEY: ${R2_READ_SECRET_ACCESS_KEY} + R2_WRITE_ACCESS_KEY_ID : ${R2_WRITE_ACCESS_KEY_ID} + R2_WRITE_SECRET_ACCESS_KEY : ${R2_WRITE_SECRET_ACCESS_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] diff --git a/docs/checkpointing.md b/docs/checkpointing.md deleted file mode 100644 index 79d379f..0000000 --- a/docs/checkpointing.md +++ /dev/null @@ -1,124 +0,0 @@ -# Checkpointing in Miners and Validators - -This document explains the checkpointing mechanism used in both miners and validators, highlighting the process and key differences between them. - -## Overview - -Checkpointing is a crucial feature that allows miners and validators to save their current state periodically. This enables them to resume operations seamlessly after interruptions, such as crashes or restarts, without losing significant progress. - -- **Miners** save their training state, including the model parameters, optimizer state, scheduler state, and current training step. -- **Validators** save their evaluation state, including the model parameters, evaluation scores, weights, and current evaluation step. - -## Asynchronous Checkpoint Saving - -Both miners and validators utilize asynchronous checkpoint saving to prevent blocking the main training or evaluation loops. By saving checkpoints asynchronously, the processes can continue their operations without waiting for the checkpoint to be saved, enhancing performance and efficiency. - -### Key Features - -- **Non-Blocking**: Checkpoint saving runs in the background, allowing the main loop to proceed without delays. -- **Regular Intervals**: - - **Miners**: Save checkpoints every **500 training steps** (`global_step`). - - **Validators**: Save checkpoints every **500 blocks** based on the blockchain's block number. - -## Checkpointing in Miners - -### What is Saved - -- **Model State**: The current state of the model's parameters. -- **Optimizer State**: State of the optimizer to resume training seamlessly. -- **Scheduler State**: If a learning rate scheduler is used. -- **Global Step**: The current training step (`global_step`). -- **Additional State**: Any other variables necessary for training. - -### Saving Mechanism - -- Checkpoints are saved asynchronously every 500 training steps. -- The saving process uses asynchronous tasks to offload the I/O operations. -- Default checkpoint file is `checkpoint-M.pth`. - -### Restoring from Checkpoint - -- On startup, the miner checks for the existence of the checkpoint file. -- If found, it loads the saved states and resumes training from the saved `global_step`. -- No additional flags are required for checkpoint loading. - -## Checkpointing in Validators - -### What is Saved - -- **Model State**: The current state of the model's parameters. -- **Global Step**: The current evaluation step (`global_step`). -- **Scores**: Evaluation scores for different miners. -- **Weights**: Assigned weights based on evaluation. -- **Additional State**: Any other variables necessary for evaluation. - -### Saving Mechanism - -- Checkpoints are saved asynchronously every 500 blocks. -- The checkpointing is triggered based on the blockchain's block number. -- Uses asynchronous tasks to prevent blocking the evaluation loop. -- Default checkpoint file is `checkpoint-V.pth`. - -### Restoring from Checkpoint - -- On startup, the validator checks for the existence of the checkpoint file. -- If found, it loads the saved states and resumes evaluation from the saved `global_step`. -- No additional flags are required for checkpoint loading. - -## Differences Between Miner and Validator Checkpoints - -| Aspect | Miner | Validator | -|----------------------|------------------------------------------------------|--------------------------------------------------------| -| **Saving Frequency** | Every 500 **training steps** (`global_step`) | Every 500 **blocks** (blockchain block number) | -| **Trigger Condition**| `global_step % 500 == 0` | `current_block % 500 == 0` | -| **Saved States** | Model state, optimizer, scheduler, global step, etc. | Model state, global step, scores, weights, etc. | -| **Checkpoint File** | `miner_checkpoint.pth` | `validator_checkpoint.pth` | -| **Restoration** | Resumes training from saved `global_step` | Resumes evaluation from saved `global_step` | - -## Configuration - -### Setting the Checkpoint Path - -By default, the checkpoint files are saved with the names `checkpoint-M.pth` and `checkpoint-V.pth`. You can customize the checkpoint path using the `--checkpoint_path` argument when running the miner or validator. - -**Example**: - -```bash -# For Miner -python neurons/miner.py --checkpoint_path /path/to/custom_miner_checkpoint.pth - -# For Validator -python neurons/validator.py --checkpoint_path /path/to/custom_validator_checkpoint.pth -``` - -### Ensure Write Permissions - -Make sure that the process has read and write permissions to the directory where the checkpoint files are stored. - -## Best Practices - -- **Regular Monitoring**: Check logs to ensure that checkpoints are being saved and loaded correctly. -- **Avoid Overwriting**: Ensure that `global_step` is not being unintentionally reset after loading from a checkpoint. -- **Backup Checkpoints**: Periodically back up checkpoint files to prevent data loss. -- **Consistent Paths**: Use consistent checkpoint paths when running multiple processes to avoid confusion. - -## Troubleshooting - -- **Checkpoint Not Saving**: - - Verify that the checkpoint path is correct. - - Ensure the process has write permissions to the checkpoint location. - - Check for any errors in the logs during the checkpoint saving steps. -- **Global Step Reset to Zero**: - - Check that `global_step` is not being reinitialized after loading the checkpoint. - - Remove any code that sets `global_step = 0` after loading. -- **Checkpoint Not Loading**: - - Ensure the checkpoint file exists at the specified path. - - Verify that the file is not corrupted. - - Check logs for any exceptions during the loading process. -- **Asynchronous Saving Issues**: - - Ensure that the event loop is running correctly. - - Check for exceptions in the asynchronous tasks. - -## Conclusion - -Checkpointing is essential for maintaining continuity in mining and validation operations. By understanding the differences and properly configuring your setup, you can ensure efficient and reliable performance of your miner and validator nodes. diff --git a/docs/global_sync.md b/docs/global_sync.md deleted file mode 100644 index 5e78f02..0000000 --- a/docs/global_sync.md +++ /dev/null @@ -1,225 +0,0 @@ -# Global Step Synchronization - - -## Introduction - -This document explains how the system achieves **global step synchronization** among miners and validators. Synchronizing the `global_step` is crucial to ensure consistent training progression, learning rate scheduling, and coordinated model updates across all nodes participating in the decentralized training process. - -## Motivation - -Without global step synchronization, several issues can arise: - -- **Learning Rate Inconsistency**: New nodes or restarted nodes may start with `global_step` at zero, causing them to be in the warm-up phase of the learning rate scheduler while others are in stable or decay phases. -- **Training Progress Discrepancies**: Nodes operating at different `global_step` values may apply updates that are out of sync, leading to conflicts and suboptimal model performance. -- **Optimization Conflicts**: Asynchronous steps can cause conflicting gradients and hinder convergence. - -To address these issues, the system implements a mechanism to synchronize `global_step` across all nodes, ensuring cohesive training and optimal model updates. - -## Synchronization Mechanism Overview - -The synchronization of `global_step` is achieved through the following steps: - -1. **Embedding `global_step` in Model Slices**: Whenever a miner or validator uploads a model slice (state or delta), they include their current `global_step` in the data. -2. **Extracting `global_step` from Received Slices**: When nodes receive slices from others, they extract the `global_step` and keep track of the maximum value. -3. **Updating Local `global_step`**: Nodes update their local `global_step` to be the maximum between their current value and the maximum `global_step` from the received slices. -4. **Adjusting Learning Rate Schedulers**: After updating `global_step`, nodes adjust their learning rate schedulers to align with the new `global_step`. - -## Detailed Implementation - -### Including `global_step` in Uploaded Slices - -When miners and validators upload their model slices, they include the `global_step` as metadata. This is implemented in the `upload_slice_for_window` function. - -```python -# In src/templar/comms.py - -async def upload_slice_for_window( - bucket: str, - model: torch.nn.Module, - window: int, - seed: str, - wallet: 'bt.wallet', - compression: int, - key: str = 'slice', - global_step: int = 0 -): - filename = f'{key}-{window}-{wallet.hotkey.ss58_address}.pt' - logger.debug(f"Uploading slice to S3: {filename}") - - # Get indices for slicing - indices = await get_indices_for_window(model, seed, compression) - - # Create the slice data with global_step - slice_data = {'global_step': global_step} - for name, param in model.named_parameters(): - slice_data[name] = param.data.view(-1)[indices[name].to(model.device)].cpu() - - # Save and upload the slice_data - # ... existing code to save and upload to S3 ... -``` - -### Extracting and Updating `global_step` from Received Slices - -When applying slices from other nodes, the system extracts `global_step` and updates the local `global_step` accordingly. - -```python -# In src/templar/comms.py - -async def apply_slices_to_model( - model: torch.nn.Module, - window: int, - seed: str, - compression: int, - key: str = 'slice' -) -> int: - indices_dict = await get_indices_for_window(model, seed, compression) - slice_files = await load_files_for_window(window=window, key=key) - - max_global_step = 0 # Initialize max_global_step - - # Iterate over each slice file - for file_i in slice_files: - try: - slice_i = await get_slices(file_i, model.device) - slice_global_step = slice_i.get('global_step', 0) # Default to 0 if not present - max_global_step = max(max_global_step, slice_global_step) - - # Apply the slice to the model - # ... existing code to apply parameter slices ... - except Exception as e: - logger.exception(f"Error applying slice from {file_i}: {e}") - - # Return the maximum global_step found - return max_global_step -``` - -### Updating Local `global_step` and Adjusting the Scheduler - -After applying slices, nodes update their local `global_step` and adjust their learning rate schedulers to reflect the new training progress. - -```python -# In neurons/miner.py or neurons/validator.py - -# Apply slices and get max_global_step -max_global_step = await apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - key='state' -) - -# Update local global_step -self.global_step = max(self.global_step, max_global_step) -self.scheduler.last_epoch = self.global_step - 1 # Update scheduler to match global_step -tplr.logger.info(f"Updated global step to {self.global_step}") -``` - -### Initializing or Loading `global_step` from Checkpoints - -When nodes start or restart, they load the `global_step` from saved checkpoints if available. - -```python -# In neurons/miner.py or neurons/validator.py - -# Load checkpoint if it exists -if os.path.exists(self.checkpoint_path): - tplr.logger.info(f"Loading checkpoint from {self.checkpoint_path}") - global_step, _ = asyncio.run(load_checkpoint( - filename=self.checkpoint_path, - model=self.model, - optimizer=self.optimizer, # For miners - scheduler=None, # Scheduler will be initialized later - device=self.config.device - )) - self.global_step = global_step - tplr.logger.info(f"Resumed from global step {self.global_step}") -else: - tplr.logger.info("No checkpoint found. Starting from scratch.") - self.global_step = 0 -``` - -### Adjusting the Learning Rate Scheduler - -When initializing the learning rate scheduler, the `last_epoch` parameter is set to `self.global_step - 1` to ensure the learning rate matches the current training stage. - -```python -# In neurons/miner.py or neurons/validator.py - -self.scheduler = get_wsd_scheduler( - optimizer=self.optimizer, - num_warmup_steps=self.hparams.num_warmup_steps, - num_stable_steps=self.hparams.num_stable_steps, - num_decay_steps=self.hparams.num_decay_steps, - last_epoch=self.global_step - 1 # Set to global_step - 1 -) -``` - -### Saving Checkpoints with `global_step` - -Nodes save their `global_step` along with other state information in checkpoints. - -```python -# In src/templar/comms.py - -async def save_checkpoint( - filename, - model, - optimizer=None, - scheduler=None, - global_step=0, - **kwargs -): - checkpoint = { - 'global_step': global_step, - 'model_state_dict': model.state_dict(), - # Include optimizer and scheduler states if available - # ... existing code ... - } - # Save the checkpoint asynchronously - await loop.run_in_executor(None, torch.save, checkpoint, filename) -``` - -## Handling Possible Scenarios - -### New Nodes Joining the Network - -- **Scenario**: A new miner or validator joins the network without prior checkpoints. -- **Handling**: - - The node starts with `global_step = 0`. - - Upon applying slices from other nodes, it updates its `global_step` to match the network. - - The learning rate scheduler is adjusted accordingly. - -### Node Restarts - -- **Scenario**: A node restarts due to a crash or manual restart. -- **Handling**: - - The node loads its saved `global_step` from the checkpoint. - - After applying new slices, it updates `global_step` if higher steps are found. - - The learning rate scheduler is realigned. - -### Missing `global_step` in Slices - -- **Scenario**: Some slices do not contain `global_step` (e.g., due to older software versions). -- **Handling**: - - Slices without `global_step` default to zero. - - The system uses the maximum `global_step` from all slices. - - Nodes avoid regressing `global_step` to a lower value. - -## Benefits of Global Step Synchronization - -- **Consistent Learning Rate Scheduling**: Ensures all nodes are in the same phase (warm-up, stable, decay) of the learning rate schedule. -- **Aligned Training Progress**: Nodes update and apply model parameters coherently. -- **Improved Model Convergence**: Synchronization reduces conflicting updates and promotes efficient training. -- **Enhanced Collaboration**: Facilitates smoother integration of contributions from various nodes. - -## Conclusion - -By embedding `global_step` within model slices and updating the local `global_step` based on the maximum received value, the system achieves effective synchronization across miners and validators. This mechanism ensures consistent training progression, coordinated updates, and optimal performance of the decentralized model training process. - ---- - -**Note**: For more details on checkpointing and learning rate scheduling, refer to the following documents: - -- [Checkpointing in Miners and Validators](checkpointing.md) -- [Learning Rate Scheduler Implementation](../src/templar/learning_rates.py) \ No newline at end of file diff --git a/docs/incentive_design.md b/docs/incentive_design.md deleted file mode 100644 index b8d6a94..0000000 --- a/docs/incentive_design.md +++ /dev/null @@ -1,203 +0,0 @@ -# Incentive Design - -## Introduction - -This document provides a detailed explanation of the incentive mechanism employed in the protocol, focusing on how miners and validators interact to collaboratively train a model while ensuring honest participation. The protocol is designed to encourage miners to contribute genuine model updates that improve the overall model performance, with validators evaluating these contributions to assign rewards accordingly. - -## Overview of the Protocol - -The protocol involves two main participants: - -- **Miners**: Nodes responsible for training the model on assigned data subsets and uploading their updates. -- **Validators**: Nodes that evaluate the miners' contributions by comparing the uploaded updates with locally computed gradients. - -The coordination between miners and validators ensures that only beneficial updates are integrated into the model, and participants are incentivized to act honestly. - -## Miners - -### Operations - -1. **Model Synchronization**: - - Miners start by synchronizing their model with the latest global state. - - They download **state slices** from other miners, ensuring their model parameters are up-to-date. - -2. **Data Acquisition**: - - Each miner receives a specific subset of the dataset (pages) for the current window. - - The data assignment is deterministic, based on a seed derived from the window number and the miner's UID. - -3. **Training**: - - Miners train their local model on the assigned data, performing gradient updates. - - The training is conducted for a specific number of steps, determined by the batch size and sequence length. - -4. **Delta Computation and Upload**: - - After training, miners compute the **delta** (the difference between the updated and initial model parameters). - - These deltas are compressed and uploaded to a designated S3 bucket associated with the miner. - -5. **Window Progression**: - - Miners proceed to the next window and repeat the process, ensuring continuous contribution to model training. - -### Formal Definitions - -- **Model Parameters**: $\theta^t$ at window $t$. -- **Updated Parameters**: $\theta^{t+1}$ after training. -- **Delta**: $\delta^t = \theta^{t+1} - \theta^t$. - -## Validators - -### Operations - -1. **Model Synchronization**: - - Validators synchronize their model to match the state at the beginning of the evaluation window. - - They download and apply **state slices** to ensure consistency. - -2. **Delta Acquisition**: - - Validators download the deltas uploaded by miners for the evaluation window. - -3. **Local Gradient Computation**: - - For each miner, the validator computes the local gradient $\hat{g}_i$ on the same data subset the miner was assigned. - -4. **Scoring**: - - Validators calculate the **cosine similarity** between each miner's delta $\delta_i$ and the validator's local gradient $\hat{g}_i$. - - This similarity score reflects how well the miner's update aligns with the true gradient. - -5. **Reward Assignment**: - - Based on the similarity scores, validators assign weights (rewards) to miners. - - These weights are normalized and set on the chain to influence the global model updates. - -### Formal Definitions - -- **Local Gradient**: $\hat{g}_i$, the gradient computed by the validator for miner $i$. -- **Miner's Delta**: $\delta_i$, uploaded by miner $i$. -- **Cosine Similarity**: - -$$ -s_i = \frac{\delta_i \cdot \hat{g}_i}{|\delta_i| |\hat{g}_i|} -$$ - -- **Assigned Weight**: $w_i$, proportional to $s_i$. - -## Incentive Mechanism - -### Objective - -The incentive mechanism aims to: - -- **Encourage Honest Participation**: Miners are motivated to perform genuine training and provide truthful updates. -- **Promote Model Improvement**: Only updates that positively contribute to the model are rewarded. -- **Discourage Malicious Behavior**: Malicious or random updates yield low or negative rewards, making dishonest behavior unprofitable. - -### Detailed Explanation - -#### Cosine Similarity Scoring - -For each miner $i$: - -1. **Compute Cosine Similarity**: - -$$ -s_i = \frac{\delta_i \cdot \hat{g}_i}{|\delta_i| |\hat{g}_i|} -$$ - - - Measures the alignment between the miner's update and the true gradient. - -2. **Interpretation of $s_i$**: - - **$s_i > 0$**: Miner’s update is in the same general direction as the true gradient, contributing positively. - - **$s_i \approx 0$**: Miner’s update is orthogonal to the true gradient, offering little to no benefit. - - **$s_i < 0$**: Miner’s update opposes the true gradient, potentially harmful. - -#### Weight Assignment - -1. **Initial Weight Calculation**: - - Assign initial weights proportional to the similarity scores: - -$$ -w_i\prime = \max(s_i, 0) -$$ - -2. **Normalization**: - - Normalize the weights to ensure they sum up to 1: - -$$ -w_i = \frac{w_i'}{\sum_j w_j'} -$$ - - This ensures the distribution of rewards is fair and proportional to positive contributions. - -#### Reward Distribution - -- **Total Reward Pool**: Determined by network parameters and available tokens. -- **Individual Reward**: - -$$ -R_i = R_{\text{total}} \times w_i -$$ - -- Miners receive rewards based on their normalized weights. - -### Formal Guarantees - -1. **Alignment Incentive**: - - Miners maximize rewards by aligning their updates with the true gradient. - - Honest training naturally leads to higher cosine similarity scores. - -2. **Robustness Against Malicious Behavior**: - - Malicious updates yield low or negative similarity scores. - - Negative scores are set to zero in weight assignment, nullifying rewards for harmful contributions. - -3. **Fair Reward Distribution**: - - Normalization ensures that rewards are proportionally distributed among positive contributors. - - Miners contributing more effectively to the model receive higher rewards. - -4. **Convergence Assurance**: - - By aggregating updates that align with the true gradients, the model is guaranteed to improve or converge under standard optimization assumptions. - -5. **Data Subset Specialization**: - - Miners focus on specific data subsets, promoting specialization and efficient coverage of the entire dataset. - -6. **Sybil Resistance**: - - Rewards are tied to the quality of contributions, not the number of identities. - - Multiple identities with low-quality updates do not gain an advantage. - -## Formal Analysis - -### Miner Utility Maximization - -Each miner seeks to maximize their expected reward \( R_i \): - -$$ -\max_{\delta_i} \quad R_i = R_{\text{total}} \times \frac{\max(s_i, 0)}{\sum_j \max(s_j, 0)} -$$ - -Subject to: - -- **Update Constraint**: $\delta_i = \theta^{t+1}_i - \theta^t$ -- **Training Dynamics**: $\theta^{t+1}_i = \theta^t - \eta \hat{g}_i$ (using learning rate $\eta$) - -The miner's optimal strategy is to set $\( \delta_i \)$ proportional to $\( -\hat{g}_i \)$, aligning with the negative gradient descent direction. - -### Validator Consistency - -Validators ensure that: - -- The evaluation is done fairly using consistent data subsets. -- The local gradients $\( \hat{g}_i \)$ are computed accurately. - -### Security Considerations - -1. **Data Integrity**: - - Data subsets are determined by deterministic functions, preventing miners from choosing favorable data. - -2. **Parameter Confidentiality**: - - Only parameter slices are shared, and the indices are not revealed in advance, reducing the risk of targeted attacks. - -3. **Resistance to Free Riders**: - - Miners not contributing meaningful updates do not receive rewards. - - Validators' scoring mechanism filters out non-beneficial contributions. - -## Conclusion - -The protocol's incentive mechanism effectively encourages miners to contribute authentic, high-quality updates to the global model. By tying rewards to the cosine similarity between miners' updates and validators' local gradients, the system ensures that only beneficial contributions are rewarded. Formal guarantees provide robustness against malicious actors and promote the overall improvement of the model through collaborative effort. - -The careful design of data assignment, update evaluation, and reward distribution creates a self-regulating ecosystem where honest participation is the most profitable strategy for miners, aligning individual incentives with the collective goal of training an effective model. diff --git a/docs/miner.md b/docs/miner.md index 285fb34..ad428b1 100644 --- a/docs/miner.md +++ b/docs/miner.md @@ -1,179 +1,308 @@ +Sure, I'll update the documentation to reflect the new recommended method using Docker Compose and ensure it's coherent following the Diátaxis framework. I'll also populate the `.env.example` file with the required variables and provide instructions for running without Docker. + +--- # Miner Setup -This document provides a guide on how to set up and run a miner using `miner.py`. It explains the workflow, configuration options, and step-by-step instructions to get a miner up and running. +This document provides a comprehensive guide on how to set up and run a miner using `miner.py`. Miners are crucial components of **τemplar**, responsible for training the model on assigned data subsets and sharing their gradients with peers. ## Table of Contents - [Miner Setup](#miner-setup) - - [Table of Contents](#table-of-contents) + - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Installation](#installation) - - [Automated Installation (WIP](#automated-installation-recommended) + - [Using Docker Compose (Recommended)](#using-docker-compose-recommended) - [Manual Installation](#manual-installation) - [Running the Miner](#running-the-miner) - - [Using PM2 (Recommended)](#using-pm2-recommended) - - [Important Flags](#important-flags) + - [Using Docker Compose](#using-docker-compose) + - [Running Without Docker](#running-without-docker) - [Configuration](#configuration) + - [Environment Variables](#environment-variables) - [Hardware Requirements](#hardware-requirements) - [Network Options](#network-options) - - [AWS Setup](#aws-setup) - [Monitoring](#monitoring) - [Logs](#logs) - [Performance](#performance) - [Troubleshooting](#troubleshooting) + - [Miner Operations](#miner-operations) + - [Model Synchronization](#model-synchronization) + - [Training Process](#training-process) + - [Gradient Sharing](#gradient-sharing) + +--- + +## Introduction + +This guide will help you set up and run a miner for **τemplar**. We'll cover both the recommended Docker Compose method and manual installation for environments where Docker is not preferred. + +--- ## Prerequisites - **NVIDIA GPU** with CUDA support - - Minimum 80GB VRAM recommended + - Minimum 24GB VRAM recommended - **Ubuntu** (or Ubuntu-based Linux distribution) -- **Python 3.12** -- **CUDA-compatible drivers** +- **Docker** and **Docker Compose** +- **Git** +- **Cloudflare R2 Bucket Configuration**: + - Permissions remain the same as before. + - **Bucket Setup**: + 1. **Create a Bucket**: Name it the same as your **account ID** and set the **region** to **ENAM**. + 2. **Generate Tokens**: + - **Read Token**: Admin Read permissions. + - **Write Token**: Admin Read & Write permissions. + 3. **Store Credentials**: You'll need these for the `.env` file. + +--- +## Installation -## Cloudflare R2 Bucket Configuration - To use buckets for sharing model slices, do the following: - 1. **Navigate to R2 Object Storage and Create a Bucket**: - - Name the bucket the same as your CloudFlare **account ID**. This can be found on the your [Cloudflare Dashboard](https://dash.cloudflare.com) in the lower right corner or the right side of the R2 Object Storage Overview page. Account IDs are not sensitive and are safe to share. - - Set the **region** to **ENAM** (Eastern North America). +### Using Docker Compose (Recommended) - 2. **Generate Tokens**: - - Navigate to the R2 Object Storage Overview page, on the left side, click "Manage R2 API Tokens". - - Create seperate **read** and **read/write** tokens. - - Note down the access key IDs and secret access keys for each token. These can also be retrieved at any time from your R2 API Token Management page - - ***Heads up***: The access key id and secret access key for your *read* token will be shared - with other neurons through commits to the network. The secrets for your write - token will stay secret. +1. **Install Docker and Docker Compose**: - 3. **Update `.env.yaml`**: - - Create the file `.env.yaml` by copying [`.env-template.yaml`](../.env-template.yaml) - and populate it with values from the previous steps: - ``` - cp .env-template.yaml .env.yaml - ``` - + ```bash + # Update package list + sudo apt-get update + # Install prerequisites + sudo apt-get install \ + ca-certificates \ + curl \ + gnupg \ + lsb-release + # Add Docker’s official GPG key + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg -## Installation + # Set up the repository + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - + # Install Docker Engine + sudo apt-get update + sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -### Manual Installation + # Install Docker Compose + sudo apt-get install docker-compose + ``` -If you prefer to install manually, follow these steps: +2. **Clone the Repository**: -1. **Install System Dependencies**: -```bash -# Add Python 3.12 repository -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt-get update + ```bash + git clone https://github.com/tplr-ai/templar.git + cd templar + ``` -# Install required packages -sudo apt-get install git python3-pip jq npm -``` +3. **Navigate to the Docker Directory**: -2. **Install Node.js and PM2**: -```bash -npm install pm2 -g && pm2 update -``` + ```bash + cd docker + ``` -3. **Install Rust and uv**: -```bash -# Install Rust -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -. "$HOME/.cargo/env" - -# Install uv and set python version to 3.12 -curl -LsSf https://astral.sh/uv/install.sh | sh -source $HOME/.local/bin/env -uv python install 3.12 && uv python pin 3.12 -``` +4. **Create and Populate the `.env` File**: -4. **Clone Repo**: -```bash -# Git Clone -git clone https://github.com/tplr-ai/templar.git -cd templar -``` + Create a `.env` file in the `docker` directory by copying the `.env.example`: + ```bash + cp .env.example .env + ``` -5. **Set Up Python Environment**: -```bash -# Create virtual environment -uv venv .venv -source .venv/bin/activate + Populate the `.env` file with your configuration. The variables that need to be set are: -# Install PyTorch -uv pip install torch --index-url https://download.pytorch.org/whl/cu118 + ```dotenv:docker/.env + WANDB_API_KEY=your_wandb_api_key -# Install requirements -uv sync --extra all -``` + # Cloudflare R2 Credentials + R2_ACCOUNT_ID=your_r2_account_id + R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id + R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key -6. **Create and Register Wallets**: -```bash -# Create coldkey -btcli wallet new_coldkey --wallet.name default --n-words 12 + R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id + R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + # Wallet Configuration + WALLET_NAME=default + WALLET_HOTKEY=your_miner_hotkey_name -# Create and register hotkey -btcli wallet new_hotkey --wallet.name default --wallet.hotkey --n-words 12 -btcli subnet pow_register --wallet.name default --wallet.hotkey --netuid --subtensor.network -``` + # Network Configuration + NETWORK=finney + NETUID=3 -7. **Log into Weights & Biases (WandB)** -```bash -# Log into WandB -wandb login -``` + # GPU Configuration + CUDA_DEVICE=cuda:0 + + # Additional Settings + DEBUG=false + ``` + + Replace the placeholders with your actual values. + +5. **Update `docker-compose.yml`**: + + Ensure that the `docker-compose.yml` file is correctly configured for your setup (usually no changes are needed). + +6. **Run Docker Compose**: + + Start the miner using Docker Compose: + + ```bash + docker-compose up -d node + ``` + + This will start the miner in detached mode. + +### Manual Installation + +If you prefer to run the miner without Docker, follow the instructions in the [Running Without Docker](#running-without-docker) section. + +--- ## Running the Miner -### Using PM2 (Recommended) +### Using Docker Compose -PM2 automatically manages your miner processes and restarts them if they crash: +Assuming you've completed the installation steps above, your miner should now be running. You can verify this by listing running containers: ```bash -# Start a miner on each GPU - pm2 start neurons/miner.py --interpreter python3 --name miner -- \ - --actual_batch_size \ - --wallet.name default \ - --wallet.hotkey "name" \ - --device "cuda" \ - --use_wandb \ - --netuid \ - --subtensor.network \ - --process_name miner \ # Must match PM2's --name - --sync_state - - -# Monitor logs -pm2 logs - -# Check status -pm2 list +docker ps ``` -> **Important**: When using PM2, the `--process_name` argument must match the PM2 process name specified by `--name`. For example, if PM2 process is named `miner_C0`, use `--process_name miner_C0`. +You should see a container named `templar-miner-`. + +### Running Without Docker + +1. **Install System Dependencies**: + + ```bash + # Add Python 3.12 repository + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + + # Install required packages + sudo apt-get install python3.12 python3.12-venv git + ``` + +2. **Install NVIDIA CUDA Drivers**: + + Install the appropriate NVIDIA CUDA drivers for your GPU. + +3. **Clone the Repository**: + + ```bash + git clone https://github.com/tplr-ai/templar.git + cd templar + ``` -### Important Flags -- **`--process_name`**: (Required) Must match the PM2 process name when using PM2 -- **`--sync_state`**: Synchronizes model state with network history -- **`--actual_batch_size`**: Set based on GPU memory: - - 80GB+ VRAM: batch size 6 -- **`--netuid`**: Network subnet ID (e.g., 223 for testnet) -- **`--subtensor.network`**: Network name (finney/test/local) -- **`--no_autoupdate`**: Disable automatic code updates +4. **Set Up Python Environment**: + + ```bash + # Create virtual environment + python3.12 -m venv .venv + source .venv/bin/activate + + # Upgrade pip + pip install --upgrade pip + + # Install PyTorch with CUDA support + pip install torch --index-url https://download.pytorch.org/whl/cu118 + + # Install other requirements + pip install -r requirements.txt + + # Install uv tool (if needed) + pip install uv + ``` + +5. **Create and Register Wallets**: + + ```bash + # Create coldkey + btcli wallet new_coldkey --wallet.name default --n-words 12 + + # Create and register hotkey + btcli wallet new_hotkey --wallet.name default --wallet.hotkey miner --n-words 12 + btcli subnet pow_register --wallet.name default --wallet.hotkey miner --netuid --subtensor.network + ``` + +6. **Log into Weights & Biases (WandB)**: + + ```bash + wandb login your_wandb_api_key + ``` + +7. **Set Environment Variables**: + + Export necessary environment variables or create a `.env` file in the project root. + + ```bash + export WANDB_API_KEY=your_wandb_api_key + export R2_ACCOUNT_ID=your_r2_account_id + export R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id + export R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key + export R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id + export R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + ``` + +8. **Run the Miner**: + + ```bash + python neurons/miner.py \ + --actual_batch_size 6 \ + --wallet.name default \ + --wallet.hotkey miner \ + --device cuda \ + --use_wandb \ + --netuid \ + --subtensor.network \ + --sync_state + ``` + +--- ## Configuration +### Environment Variables + +When using Docker Compose, set the following variables in the `docker/.env` file: + +```dotenv:docker/.env +WANDB_API_KEY=your_wandb_api_key + +# Cloudflare R2 Credentials +R2_ACCOUNT_ID=your_r2_account_id + +R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id +R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key + +R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id +R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + +# Wallet Configuration +WALLET_NAME=default +WALLET_HOTKEY=your_miner_hotkey_name + +# Network Configuration +NETWORK=finney +NETUID=3 + +# GPU Configuration +CUDA_DEVICE=cuda:0 + +# Additional Settings +DEBUG=false +``` + +**Note**: The R2 permissions remain unchanged from previous configurations. + ### Hardware Requirements -- **GPU Memory Requirements**: - - Recommended: 80GB+ VRAM +- **GPU Requirements**: + - Minimum: NVIDIA H100 with 80GB VRAM - **Storage**: 100GB+ recommended for model and data - **RAM**: 32GB+ recommended - **Network**: Stable internet connection with good bandwidth @@ -182,36 +311,66 @@ pm2 list - **Mainnet (Finney)**: - Network: `finney` - - Netuid: 3 + - Netuid: `3` - **Testnet**: - Network: `test` - - Netuid: 223 - - Endpoint: `wss://test.finney.opentensor.ai:443/` + - Netuid: `223` - **Local**: - Network: `local` - - Netuid: 3 - - Endpoint: `wss://localhost:9944` + - Netuid: `1` + +--- ## Monitoring ### Logs -- **PM2 Logs**: `pm2 logs [miner_name]` -- **System Monitoring**: `pm2 monit` -- **Weights & Biases**: Enable with `--use_wandb` +- **Docker Logs**: + + ```bash + docker logs -f templar-miner-${WALLET_HOTKEY} + ``` + +- **Weights & Biases**: + + - Ensure `--use_wandb` is enabled + - Monitor training metrics and performance on your WandB dashboard ### Performance -Monitor key metrics: +Keep an eye on: + - GPU utilization - Memory usage - Network bandwidth - Training progress - Rewards and weights - +--- diff --git a/docs/validator.md b/docs/validator.md index 7f00fd7..6df5147 100644 --- a/docs/validator.md +++ b/docs/validator.md @@ -1,198 +1,250 @@ # Validator Setup -This document provides a guide on how to set up and run a validator using `validator.py`. Validators are crucial components of the protocol, responsible for evaluating miners' contributions by comparing their uploaded deltas with locally computed gradients. +This document provides a comprehensive guide on how to set up and run a validator using `validator.py`. Validators are crucial components of **τemplar**, responsible for evaluating miners' contributions by assessing their uploaded gradients. ## Table of Contents - [Validator Setup](#validator-setup) - - [Table of Contents](#table-of-contents) + - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Installation](#installation) - - [Automated Installation (Recommended)](#automated-installation-recommended) + - [Using Docker Compose (Recommended)](#using-docker-compose-recommended) - [Manual Installation](#manual-installation) - [Running the Validator](#running-the-validator) - - [Using PM2 (Recommended)](#using-pm2-recommended) - - [Important Flags](#important-flags) + - [Using Docker Compose](#using-docker-compose) + - [Running Without Docker](#running-without-docker) - [Configuration](#configuration) + - [Environment Variables](#environment-variables) - [Hardware Requirements](#hardware-requirements) - [Network Options](#network-options) - - [AWS Setup](#aws-setup) - [Monitoring](#monitoring) - [Logs](#logs) - [Performance](#performance) + - [Troubleshooting](#troubleshooting) - [Validator Operations](#validator-operations) - [State Synchronization](#state-synchronization) - [Evaluation Process](#evaluation-process) - [Weight Setting](#weight-setting) - - [Troubleshooting](#troubleshooting) + +--- + +## Introduction + +This guide will help you set up and run a validator for **τemplar**. Validators play a critical role in maintaining the integrity of the network by evaluating miners' contributions and updating weights accordingly. + +--- ## Prerequisites - **NVIDIA GPU** with CUDA support - Minimum 24GB VRAM recommended - - Single GPU typically sufficient - **Ubuntu** (or Ubuntu-based Linux distribution) -- **Python 3.12** -- **CUDA-compatible drivers** +- **Docker** and **Docker Compose** +- **Git** - **Cloudflare R2 Bucket Configuration**: - - To use buckets for sharing model slices, do the following: - 1. **Navigate to R2 Object Storage and Create a Bucket**: - - Name the bucket the same as your **account ID**. - - Set the **region** to **ENAM**. - - - 2. **Generate Tokens**: - - Create a **read token** with **Admin Read** permissions and a **write token** with **Admin Read & Write** permissions. - - Note down the access key IDs and secret access keys for each token. - - 3. **Update `.env.yaml`**: - - Create the file `.env.yaml` by copying [`.env-template.yaml`](../.env-template.yaml) - and populate it with values from the previous steps: - ``` - cp .env-template.yaml .env.yaml - ``` - - The access key id and secret access key for your *read* token will be shared - with other neurons through commits to the network. The secrets for your write - token will stay secret. + - Permissions remain the same as before. + - **Bucket Setup**: + 1. **Create a Bucket**: Name it the same as your **account ID** and set the **region** to **ENAM**. + 2. **Generate Tokens**: + - **Read Token**: Admin Read permissions. + - **Write Token**: Admin Read & Write permissions. + 3. **Store Credentials**: You'll need these for the `.env` file. -- **Git** +--- ## Installation - + # GPU Configuration + CUDA_DEVICE=cuda:0 + + # Node Type + NODE_TYPE=validator + + # Additional Settings + DEBUG=false + ``` + + **Note**: Set `NODE_TYPE` to `validator`. + +5. **Update `docker-compose.yml`**: + + Ensure that the `docker-compose.yml` file is correctly configured for your setup. + +6. **Run Docker Compose**: + + Start the validator using Docker Compose: + + ```bash + docker-compose up -d node + ``` ### Manual Installation -If you prefer to install manually, follow these steps: +If you prefer to run the validator without Docker, follow the instructions in the [Running Without Docker](#running-without-docker) section. -1. **Install System Dependencies**: -```bash -# Add Python 3.12 repository -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt-get update +--- -# Install required packages -sudo apt-get install python3.12 python3.12-venv git npm -``` +## Running the Validator + +### Using Docker Compose + +After completing the installation steps, your validator should be running. Check it with: -2. **Install Node.js and PM2**: ```bash -curl -fsSL https://deb.nodesource.com/setup_18.x | sudo bash -sudo apt-get install -y nodejs -npm install pm2 -g +docker ps ``` -3. **Install Rust and uv**: -```bash -# Install Rust -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env +You should see a container named `templar-validator-`. -# Install uv -curl -LsSf https://astral.sh/uv/install.sh | sh -``` +### Running Without Docker -4. **Set Up Python Environment**: -```bash -# Create virtual environment -uv venv .venv -source .venv/bin/activate +1. **Install System Dependencies**: -# Install PyTorch -uv pip install torch --index-url https://download.pytorch.org/whl/cu118 + Same as in the miner setup. -# Install requirements -uv sync --extra all -``` +2. **Install NVIDIA CUDA Drivers**: + + Install the appropriate NVIDIA CUDA drivers. + +3. **Clone the Repository**: + + ```bash + git clone https://github.com/tplr-ai/templar.git + cd templar + ``` + +4. **Set Up Python Environment**: + + Same as in the miner setup. 5. **Create and Register Validator Wallet**: -```bash -# Create coldkey -btcli wallet new_coldkey --wallet.name default --n-words 12 -# Create and register validator hotkey -btcli wallet new_hotkey --wallet.name default --wallet.hotkey validator --n-words 12 -btcli subnet pow_register --wallet.name default --wallet.hotkey validator --netuid --subtensor.network -``` + ```bash + # Create coldkey if not already created + btcli wallet new_coldkey --wallet.name default --n-words 12 -6. **Log into Weights & Biases (WandB)** -```bash -# Log into WandB -wandb login -``` + # Create and register validator hotkey + btcli wallet new_hotkey --wallet.name default --wallet.hotkey validator --n-words 12 + btcli subnet pow_register --wallet.name default --wallet.hotkey validator --netuid --subtensor.network + ``` -## Running the Validator +6. **Log into Weights & Biases (WandB)**: -### Using PM2 (Recommended) + ```bash + wandb login your_wandb_api_key + ``` -PM2 automatically manages your validator process and restarts it if it crashes: +7. **Set Environment Variables**: -```bash -pm2 start neurons/validator.py --interpreter python3 --name validator -- \ - --actual_batch_size 6 \ - --wallet.name default \ - --wallet.hotkey validator \ - --use_wandb \ - --netuid \ - --subtensor.network \ - --process_name validator \ # Must match PM2's --name - --sync_state - -> **Important**: When using PM2, the `--process_name` argument must match the PM2 process name specified by `--name`. In this example, PM2 process is named `validator`, so we use `--process_name validator`. - -# Monitor logs -pm2 logs validator - -# Check status -pm2 list -``` + Export necessary environment variables as in the miner setup. + +8. **Run the Validator**: -### Important Flags + ```bash + python neurons/validator.py \ + --actual_batch_size 6 \ + --wallet.name default \ + --wallet.hotkey validator \ + --device cuda \ + --use_wandb \ + --netuid \ + --subtensor.network \ + --sync_state + ``` -- **`--process_name`**: (Required) Must match the PM2 process name when using PM2 -- **`--sync_state`**: Synchronizes model state with network history (crucial) -- **`--actual_batch_size`**: Set based on GPU memory: - - 80GB+ VRAM: batch size 6 - - 40GB VRAM: batch size 3 - - 24GB VRAM: batch size 1 -- **`--netuid`**: Network subnet ID (e.g., 223 for testnet) -- **`--subtensor.network`**: Network name (finney/test/local) -- **`--no_autoupdate`**: Disable automatic code updates +--- ## Configuration +### Environment Variables + +Set the following in the `docker/.env` file when using Docker Compose: + +```dotenv:docker/.env +WANDB_API_KEY=your_wandb_api_key + +# Cloudflare R2 Credentials +R2_ACCOUNT_ID=your_r2_account_id + +R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id +R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key + +R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id +R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + +# Wallet Configuration +WALLET_NAME=default +WALLET_HOTKEY=your_validator_hotkey_name + +# Network Configuration +NETWORK=finney +NETUID=3 + +# GPU Configuration +CUDA_DEVICE=cuda:0 + +# Node Type +NODE_TYPE=validator + +# Additional Settings +DEBUG=false +``` + +**Note**: The R2 permissions remain unchanged. + ### Hardware Requirements -- **GPU Memory Requirements**: - - Minimum: 24GB VRAM - - Recommended: 40GB VRAM - - Optimal: 80GB VRAM +- **GPU Requirements**: + - Minimum: NVIDIA H100 with 80GB VRAM - **Storage**: 200GB+ recommended for model and evaluation data - **RAM**: 32GB+ recommended - **Network**: High-bandwidth, stable connection for state synchronization @@ -201,65 +253,113 @@ pm2 list - **Mainnet (Finney)**: - Network: `finney` - - Netuid: 3 + - Netuid: `3` - **Testnet**: - Network: `test` - - Netuid: 223 - - Endpoint: `wss://test.finney.opentensor.ai:443/` + - Netuid: `223` - **Local**: - Network: `local` - - Netuid: 1 - - Endpoint: `wss://localhost:9944` + - Netuid: `1` + +--- ## Monitoring ### Logs -- **PM2 Logs**: `pm2 logs validator` -- **System Monitoring**: `pm2 monit` -- **Weights & Biases**: Enable with `--use_wandb` - - Training metrics - - Evaluation scores - - Network statistics +- **Docker Logs**: + + ```bash + docker logs -f templar-validator-${WALLET_HOTKEY} + ``` + +- **Weights & Biases**: + + - Ensure `--use_wandb` is enabled + - Monitor evaluation metrics and network statistics ### Performance -Monitor key metrics: +Key metrics to monitor: + - GPU utilization - Memory usage - Network bandwidth - Evaluation throughput - Weight setting frequency +--- + +## Troubleshooting + +- **State Synchronization Failures**: Check network settings and ensure the validator is properly registered and connected. +- **Out of Memory Errors**: Reduce `--actual_batch_size`. +- **Network Connectivity Issues**: Verify firewall settings and network configurations. + +--- + ## Validator Operations ### State Synchronization -- Initial sync downloads full model state -- Continuous sync with new updates -- Delta application and verification -- State consistency checks +- The validator synchronizes its model with the latest global state. +- It gathers and applies gradients from miners to maintain consistency. ### Evaluation Process -1. Download miner deltas -2. Compute local gradients -3. Compare and score contributions -4. Update weights based on quality +1. **Collect Miner Gradients**: Gathers compressed gradients submitted by miners. +2. **Evaluate Contributions**: Assesses the impact of each miner's gradient on model performance. +3. **Compute Scores**: Calculates scores based on loss improvement. +4. **Update Weights**: Adjusts miners' weights on the blockchain accordingly. ### Weight Setting -- Scoring mechanisms -- Weight update frequency -- Impact on network consensus -- Optimization strategies +- **Scoring Mechanism**: Based on the performance improvement contributed by miners. +- **Update Frequency**: Weights are periodically updated on the blockchain. +- **Impact**: Influences reward distribution and miner reputation in the network. + +--- + +# `.env.example` + +Here's the updated `.env.example` file populated with all the required variables: + +```dotenv:docker/.env.example +# Weights & Biases API Key +WANDB_API_KEY=your_wandb_api_key + +# Cloudflare R2 Credentials +R2_ACCOUNT_ID=your_r2_account_id + +# Read Token (Shared with other neurons) +R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id +R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key + +# Write Token (Keep Secret) +R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id +R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + +# Wallet Configuration +WALLET_NAME=default +WALLET_HOTKEY=your_hotkey_name + +# Network Configuration +NETWORK=finney +NETUID=3 + +# GPU Configuration +CUDA_DEVICE=cuda:0 + +# Node Type (miner or validator) +NODE_TYPE=miner + +# Additional Settings +DEBUG=false +``` - +- Replace all placeholders with your actual credentials. +- The `.env` file should be placed in the `docker` directory (`templar/docker/.env`). +- Permissions for R2 remain the same; read credentials are shared, write credentials are kept secret. diff --git a/hparams.json b/hparams.json index cf500ed..64c717c 100644 --- a/hparams.json +++ b/hparams.json @@ -1,36 +1,28 @@ { - "epoch_length": 250000, - "compression": 100, + "spec_version": 5, + "project": "dough", "sequence_length": 2048, + "pages_per_window": 2, + "batch_size": 6, + "learning_rate": 4e-4, + "blocks_per_window": 3, + "windows_per_sync": 100, + "windows_per_weights": 10, + "momentum_decay": 0.999, + "topk_compression": 32, + "target_chunk": 64, + "scores_alpha": 0.001, "tokenizer_name": "togethercomputer/LLaMA-2-7B-32K", - "num_hidden_layers": 16, "hidden_size": 2048, - "intermediate_size": 8192, + "num_hidden_layers": 16, "num_attention_heads": 8, + "intermediate_size": 8192, "num_key_value_heads": 8, "activation_function": "swiGLU", "max_position_embeddings": 2048, - "mixed_precision_param": "bfloat16", - "mixed_precision_reduce": "float32", - "window_length": 3, - "desired_batch_size": 512, - "learning_rate": 7e-5, - "optimizer_beta1": 0.9, - "optimizer_beta2": 0.95, - "optimizer_weight_decay": 0.1, - "grad_clip": 1.0, - "cosine_epoch_length": 5000, - "num_warmup_steps": 1000, - "num_stable_steps": 1000000000000, - "num_decay_steps": 5000, - "eta_min": 1e-05, - "max_history": 10, - "window_speed": 100, - "validator_moving_alpha": 0.85, - "validator_norm_regularization": 0.01, - "validator_weights_temperature": 10, - "validator_window_eval_size": 3, - "validator_sample_rate": 0.01, - "validator_non_submission_decay": 0.9, - "validator_learning_rate_scale_factor": 0.1 + "weight_decay": 0.1, + "warmup_steps": 250, + "alpha_f": 0.1, + "t_max": 20000, + "validator_offset": 4 } \ No newline at end of file diff --git a/justfile b/justfile deleted file mode 100644 index 5dff88e..0000000 --- a/justfile +++ /dev/null @@ -1,73 +0,0 @@ -# Start all miners -start: - pm2 start neurons/miner.py --interpreter python3 --name M1 -- --actual_batch_size 6 --wallet.name temple_run --wallet.hotkey temple_runner --device cuda:6 --use_wandb --autoupdate --process_name M1 --sync - pm2 start neurons/miner.py --interpreter python3 --name M2 -- --actual_batch_size 6 --wallet.name noa-d --wallet.hotkey C0 --device cuda:5 --use_wandb --autoupdate --process_name M2 --sync - pm2 start neurons/miner.py --interpreter python3 --name M3 -- --actual_batch_size 6 --wallet.name noa-d --wallet.hotkey C1 --device cuda:4 --use_wandb --autoupdate --process_name M3 --sync - - -# Stop all miners -stop: - pm2 delete M1 M2 M3 - -# Stop all miners -restart: - pm2 restart M1 M2 M3 - -# Restart all miners with clean state -restart-clean: stop restart - - -# Start testnet miners and validator -start-testnet project='gonso-2': - pm2 start neurons/miner.py --interpreter python3 --name TM1 -- --actual_batch_size 6 --wallet.name Bistro --wallet.hotkey M111 --device cuda:1 --use_wandb --autoupdate --process_name TM1 --project {{project}} --netuid 223 --test - pm2 start neurons/miner.py --interpreter python3 --name TM2 -- --actual_batch_size 6 --wallet.name Bistro --wallet.hotkey M222 --device cuda:2 --use_wandb --autoupdate --process_name TM2 --project {{project}} --netuid 223 --test - pm2 start neurons/validator.py --interpreter python3 --name TV1 -- --actual_batch_size 6 --wallet.name Bistro --wallet.hotkey V11 --device cuda:3 --use_wandb --autoupdate --process_name TV1 --project {{project}} --netuid 223 --test - -# Stop testnet miners and validator -stop-testnet: - pm2 delete TM1 TM2 TV1 - -# Restart testnet miners and validator -restart-testnet: - pm2 restart TM1 TM2 TV1 - -# Remove checkpoint folder -clean-checkpoints: - rm -rf checkpoints/ - -# Clean testnet data (R2 bucket and local folders) -clean-testnet: - python3 scripts/clean_testnet.py - -# Restart testnet with clean state -restart-testnet-clean project='gonso-2': stop-testnet clean-testnet - just start-testnet {{project}} - -# Run tests with uv -test: - export NEST_ASYNCIO=1 && uv run pytest - -# Run ruff linting -lint: - ruff check --fix - -# Run code formatting -format: - ruff format - -# Run all code quality checks and tests -check: format lint test - -# Stop evaluator -stop-eval: - pm2 delete Eval - -# Start evaluator -start-eval: - pm2 start scripts/eval.py --interpreter python3 --name Eval -- --actual_batch_size 6 --device cuda:0 --use_wandb --process_name Eval - -# Restart evaluator -restart-eval: - pm2 restart Eval - - diff --git a/neurons/miner.py b/neurons/miner.py index 7383d4b..78f4b07 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -2,600 +2,400 @@ # © 2024 templar.tech # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. # fmt: off -# ruff: noqa - -# Global imports. +# Standard library import sys -import time -import wandb -import torch +import time import random import asyncio import argparse import threading + +# Third party +import torch import numpy as np -from tqdm import tqdm import bittensor as bt -import torch.optim as optim +from torch.optim import SGD from transformers import LlamaForCausalLM -from rich.markup import escape -import os -import tempfile - -# Import local files. -import templar as tplr - -# GPU optimizations. -torch.backends.cudnn.benchmark = True +from torch.optim.lr_scheduler import ( + CosineAnnealingWarmRestarts, + LinearLR, + SequentialLR, +) + +# Local +import tplr + + +# GPU optimizations +torch.manual_seed(42) +torch.cuda.manual_seed_all(42) +np.random.seed(42) +random.seed(42) +torch.backends.cudnn.deterministic = True +torch.backends.cudnn.benchmark = False torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True -class Miner: +class Miner: + + # Command line config items. @staticmethod def config(): parser = argparse.ArgumentParser(description='Miner script') - parser.add_argument('--project', type=str, default='templar', help='Optional wandb project name') - parser.add_argument('--netuid', type=int, default=3, help='Bittensor network UID.') - parser.add_argument('--actual_batch_size', type=int, default=8, help='Training batch size per accumulation.') - parser.add_argument('--device', type=str, default='cuda', help='Device to use for training (e.g., cpu or cuda)') + parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') + parser.add_argument('--project', type=str, default='templar', help='Wandb project.') + parser.add_argument('--device', type=str, default='cuda', help='Device to use for training') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--trace', action='store_true', help='Enable trace logging') - parser.add_argument('--random', action='store_true', help='Train on random') - parser.add_argument('--sync_state', action='store_true', help='Syncs the model state by pulling from the history.') - parser.add_argument('--baseline', action='store_true', help='Dont perform syncing with other peers, just train.') - parser.add_argument('--test', action='store_true', help='Run on test network') - parser.add_argument('--local', action='store_true', help='Run on local network') - parser.add_argument('--no_autoupdate', action='store_true', help='Disable automatic updates') - parser.add_argument('--checkpoint_path', type=str, default=None, help='Path to save/load the checkpoint. If None, the path is set to checkpoint-M.pth.') - parser.add_argument('--save-location', type=str, default=None, help='Directory to save/load slice files') - bt.wallet.add_args(parser) + parser.add_argument('--peers', type=int, nargs='+', default=[], help='List of UIDs to peer with') bt.subtensor.add_args(parser) + bt.logging.add_args(parser) + bt.wallet.add_args(parser) config = bt.config(parser) - if config.test: - config.subtensor.network = 'test' - config.subtensor.chain_endpoint = 'wss://test.finney.opentensor.ai:443/' - elif config.local: - config.subtensor.network = 'local' - config.subtensor.chain_endpoint = 'ws://127.0.0.1:9944' if config.debug: tplr.debug() if config.trace: tplr.trace() - if not config.no_autoupdate: - autoupdater = tplr.AutoUpdate() - autoupdater.daemon = True # Ensure thread exits when main program exits - autoupdater.start() return config - - + def __init__(self): - # Init config. + tplr.logger.debug("Starting initialization...") + + # Init config and load hparams self.config = Miner.config() - tplr.logger.info('\n' + '-' * 40 + ' Config ' + '-' * 40) - tplr.logger.info(self.config) - - # Init bittensor objects. + self.hparams = tplr.load_hparams() + + # Init bittensor objects self.wallet = bt.wallet(config=self.config) self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(netuid=self.config.netuid) - self.chain_manager = tplr.chain.ChainManager( - subtensor=self.subtensor, wallet=self.wallet, netuid=self.config.netuid - ) + self.metagraph = self.subtensor.metagraph(self.config.netuid) if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: - tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]. You need to register first with: [blue]`btcli subnet register`[/blue]\n') + tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]') sys.exit() self.uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) - tplr.logger.info('\n' + '-' * 40 + ' Objects ' + '-' * 40) - tplr.logger.info(f'\nWallet: {self.wallet}\nSubtensor: {self.subtensor}\nMetagraph: {self.metagraph}\nUID: {self.uid}') - - # Set checkpoint path - if self.config.checkpoint_path is None: - # Default path if none provided - self.checkpoint_path = f"checkpoints/checkpoint-M{self.uid}.pth" - else: - self.checkpoint_path = self.config.checkpoint_path - - # Create checkpoint directory if it doesn't exist - os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) - - # Init bucket. - try: - tplr.logger.debug(f'bucket_name: {tplr.config.BUCKET_SECRETS["bucket_name"]}') - commitment = self.chain_manager.get_commitment(self.uid) - - # Convert Bucket object back to concatenated string format for comparison - commitment_str = commitment.name + commitment.access_key_id + commitment.secret_access_key - - current_bucket = ( - tplr.config.BUCKET_SECRETS["bucket_name"] + - tplr.config.BUCKET_SECRETS["read"]["access_key_id"] + - tplr.config.BUCKET_SECRETS["read"]["secret_access_key"] - ) - tplr.logger.debug(f'Comparing:\nCommitment: {commitment_str}\nCurrent: {current_bucket}') - - if current_bucket != commitment_str: - raise ValueError("Bucket commitment data does not match.") - - except Exception as e: - tplr.logger.error(f"Commitment error: {str(e)}") - tplr.commit(self.subtensor, self.wallet, self.config.netuid) - - # Init Wandb. - # Ensure the wandb directory exists - wandb_dir = os.path.join(os.getcwd(), 'wandb') - os.makedirs(wandb_dir, exist_ok=True) - - # Define the run ID file path inside the wandb directory - run_id_file = os.path.join(wandb_dir, f"wandb_run_id_M{self.uid}_{tplr.__version__}.txt") - - # Attempt to read the existing run ID - if os.path.exists(run_id_file): - with open(run_id_file, 'r') as f: - run_id = f.read().strip() - tplr.logger.info(f"Resuming WandB run with id {run_id}") - else: - run_id = None - tplr.logger.info("Starting a new WandB run.") - - # Initialize WandB - self.wandb = tplr.initialize_wandb( - run_prefix='M', - uid=self.uid, - config=self.config, - group='miner', - job_type='training' - ) - - # Init model. - tplr.logger.info('\n' + '-' * 40 + ' Hparams ' + '-' * 40) - self.hparams = tplr.load_hparams() - torch.manual_seed(42) - np.random.seed(42) - random.seed(42) - self.model = LlamaForCausalLM(config=self.hparams.model_config) + + # Init model with hparams config + self.model = LlamaForCausalLM(self.hparams.model_config) self.model.to(self.config.device) - self.model.train() - - self.optimizer = optim.AdamW( - self.model.parameters(), - lr=self.hparams.learning_rate, # Peak learning rate - betas=(self.hparams.optimizer_beta1, self.hparams.optimizer_beta2), # B1 and B2 - weight_decay=self.hparams.optimizer_weight_decay, # Weight decay - foreach=True, # more memory usage, but faster + self.tokenizer = self.hparams.tokenizer + + # Init optimizer and momentum + self.optimizer = SGD(self.model.parameters(), lr=self.hparams.learning_rate) + self.momentum = {} + for n, p in self.model.named_parameters(): + self.momentum[n] = torch.zeros_like(p) + + # Set up scheduler + warmup_scheduler = LinearLR( + self.optimizer, + start_factor=0.1, + end_factor=1.0, + total_iters=10, ) - - # Initialize learning rate scheduler - self.scheduler = tplr.get_wsd_scheduler( - optimizer=self.optimizer, - num_warmup_steps=self.hparams.num_warmup_steps, - num_stable_steps=self.hparams.num_stable_steps, - num_decay_steps=self.hparams.num_decay_steps, + cosine_scheduler = CosineAnnealingWarmRestarts( + self.optimizer, + T_0=1000, + T_mult=2, + eta_min=self.hparams.learning_rate * 0.1, ) - - # Retrieve bucket info for all neurons - self.buckets = tplr.get_all_buckets( - netuid=self.config.netuid, - metagraph=self.metagraph, - config= self.config + self.scheduler = SequentialLR( + self.optimizer, + schedulers=[warmup_scheduler, cosine_scheduler], + milestones=[10], ) + # Init compression + self.transformer = tplr.compress.TransformDCT( + self.model, + target_chunk=self.hparams.target_chunk, + ) + self.compressor = tplr.compress.CompressDCT() - - # Initialize checkpoint manager - self.checkpoint_manager = tplr.CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, + # Init comms + self.comms = tplr.comms.Comms( wallet=self.wallet, - device=self.config.device, - optimizer=self.optimizer, - scheduler=self.scheduler - ) - - # Load initial checkpoint - self.global_step = asyncio.run( - self.checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets, - optimizer=self.optimizer, - scheduler=self.scheduler, - is_validator=False, - hparams=self.hparams - ) + save_location='/tmp', + key_prefix='model', + config=self.config, + netuid=self.config.netuid, + metagraph=self.metagraph, + hparams=self.hparams, ) - # Init run state. - self.sample_rate = 1.0 - self.current_block = self.subtensor.block - self.current_window = self.block_to_window( self.current_block ) - self.window_seeds = {self.current_window: self.window_to_seed( self.current_window) } - self.new_block_event = asyncio.Event() - self.new_window_event = asyncio.Event() - self.stop_event = asyncio.Event() - self.last_full_steps = self.hparams.desired_batch_size // self.config.actual_batch_size - if self.config.save_location is None: - # Default to system temp dir with unique neuron directory - self.save_location = os.path.join( - tempfile.gettempdir(), f"neuron_{self.wallet.hotkey.ss58_address}" - ) + # Init peers + if not self.config.peers: + self.peers = self.comms.peers + tplr.logger.info(f'Filtered peers with buckets: {self.peers}') else: - # Append neuron-specific directory to save_location - self.save_location = os.path.join( - self.config.save_location, f"neuron_{self.wallet.hotkey.ss58_address}" - ) - - # Create the directory if it doesn't exist - os.makedirs(self.save_location, exist_ok=True) - self.checkpoint_tasks = set() - print ( self.hparams ) - - async def update(self): - """Continuously updates the global state by polling every 10 minutes.""" - await asyncio.sleep(600) # Initial sleep before starting updates - while not self.stop_event.is_set(): - st = tplr.T() - await self.perform_update() - tplr.logger.info(f"{tplr.P(self.current_window, tplr.T() - st)} Updated global state.") - await asyncio.sleep(600) - - async def perform_update(self): - """Updates subtensor connection, metagraph, hyperparameters, and buckets.""" - self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(self.config.netuid) + self.peers = self.config.peers + if self.uid not in self.peers: + self.peers.append(self.uid) + # Init state params + self.stop_event = asyncio.Event() + self.current_block = self.subtensor.block + self.current_window = int(self.current_block / self.hparams.blocks_per_window) + self.step_counter = 0 - # Fetch all commitments at once - buckets = tplr.get_all_commitments( - substrate=self.subtensor.substrate, - netuid=self.config.netuid, - metagraph=self.metagraph + # Add step tracking + self.global_step = 0 + self.window_step = 0 + + # Track additional metrics + self.total_tokens_processed = 0 + self.batch_times = [] # For tracking processing speed + + # Initialize WandB + self.wandb = tplr.initialize_wandb( + run_prefix='M', + uid=self.uid, + config=self.config, + group='miner', + job_type='mining' ) - self.buckets = [] - for uid in self.metagraph.uids: - bucket = buckets.get(uid) - if isinstance(bucket, bytes): - bucket = bucket.decode('utf-8') - if bucket is not None: - tplr.logger.debug(f"UID {uid}: Valid bucket found: {bucket}") - self.buckets.append(bucket) - else: - tplr.logger.debug(f"UID {uid}: Invalid or missing bucket: {bucket}") - self.buckets.append(None) - - async def load_checkpoint_background(self): - """Handles checkpoint loading in the background.""" - try: - tplr.logger.info(f"Loading checkpoint at step {self.global_step}") - - # Load the checkpoint into a temporary model - temp_model = LlamaForCausalLM(config=self.hparams.model_config).to(self.config.device) - temp_optimizer = optim.AdamW( - temp_model.parameters(), - lr=self.hparams.learning_rate, - betas=(self.hparams.optimizer_beta1, self.hparams.optimizer_beta2), - weight_decay=self.hparams.optimizer_weight_decay, - foreach=True, - ) - temp_scheduler = tplr.get_wsd_scheduler( - optimizer=temp_optimizer, - num_warmup_steps=self.hparams.num_warmup_steps, - num_stable_steps=self.hparams.num_stable_steps, - num_decay_steps=self.hparams.num_decay_steps, - ) - temp_checkpoint_manager = tplr.CheckpointManager( - model=temp_model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device=self.config.device, - optimizer=temp_optimizer, - scheduler=temp_scheduler - ) - - # Load the checkpoint from the highest stake - await temp_checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets - ) - - # Safely update the main model's parameters - for param, temp_param in zip(self.model.parameters(), temp_model.parameters()): - param.data.copy_(temp_param.data) - - tplr.logger.info(f"Checkpoint loaded at step {self.global_step}") - - # Clean up the temporary model to free memory - del temp_model, temp_optimizer, temp_scheduler, temp_checkpoint_manager - torch.cuda.empty_cache() - - except Exception as e: - tplr.logger.error(f"Error loading checkpoint in background: {str(e)}") - + # Main training loop. async def run(self): - # Main loop. - self.loop = asyncio.get_running_loop() - self.update_task = asyncio.create_task(self.update()) - self.listener = threading.Thread(target=self.block_listener, args=(self.loop,), daemon=True).start() - self.checkpoint_tasks = set() # Track checkpoint tasks - - # Optionally sync the model state by pulling model states from the history. - if self.config.sync_state: - st = tplr.T() - history_windows = [self.current_window - i for i in range(self.hparams.max_history - 1, -1, -1)] - for window in tqdm(history_windows, desc="Syncing state"): - max_global_step, _ = await tplr.apply_slices_to_model( - model = self.model, - window = window, - seed = window, - compression = self.hparams.compression, - save_location=self.save_location, - key = 'state' + # Try to load latest checkpoint + validator_uid, stake = self.comms.get_highest_stake_validator() + if stake > 0: + try: + state_dict = await self.comms.get( + uid=str(validator_uid), + window=self.current_window, + key='checkpoint', + timeout=240, + local=False, + stale_retention=10 ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - self.scheduler.last_epoch = self.global_step - 1 # Update scheduler - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied history and updated global step to {self.global_step}.") - torch.cuda.empty_cache() - try: - # Main training loop. - while True: - try: - # Start the window step. - tplr.logger.info('[bold]' + '\n' + '-' * 40 + f' Step: {self.global_step} ' + '-' * 40) - self.global_step += 1 - start_step = tplr.T() - window = self.current_window - - # Run for non-baseline miners. - if not self.config.baseline: - st = tplr.T() - valid_buckets = [b for b in self.buckets if b is not None] - - if not valid_buckets: - tplr.logger.info(f"No valid buckets to download state slices for window {window}") - # Wait for the next window - while self.current_window == window: - await asyncio.sleep(0.1) - continue - - state_slices = await tplr.download_slices_for_buckets_and_windows( - buckets=valid_buckets, - windows=[window], - key='state', - save_location=self.save_location - ) - - n_state_slices = len(state_slices[window]) if window in state_slices else 0 - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded {n_state_slices} window states.") - - # Download the delta from the previous window. - st = tplr.T() - delta_slices = await tplr.download_slices_for_buckets_and_windows( - buckets = self.buckets, - windows = [ window - 1 ], - key = 'delta', - save_location=self.save_location - ) - n_slices = len(delta_slices[ window - 1 ]) if window - 1 in delta_slices else 0 - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Download {n_slices} window deltas.") + if state_dict is not None: + self.model.load_state_dict(state_dict) + tplr.logger.info(f"Loaded checkpoint from validator {validator_uid} at window {self.current_window}") + else: + tplr.logger.info("No checkpoint found, starting from scratch") + except Exception as e: + tplr.logger.warning(f"Failed to load checkpoint: {e}") + else: + tplr.logger.info("No active validators found, starting from scratch") - # Apply the state for the current window. - st = tplr.T() - max_global_step, _ = await tplr.apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - save_location=self.save_location, - key='state' + # Start background block listener + self.loop = asyncio.get_running_loop() + self.listener = threading.Thread( + target=self.block_listener, + args=(self.loop,), + daemon=True, + ).start() + + while True: + step_window = self.current_window + tplr.logger.info(f"\n{'-' * 40} Window: {step_window} {'-' * 40}") + + # Get the pages for this window. + pages = await tplr.dataset.DatasetLoader.next_pages( + offset = step_window, + n_pages = self.hparams.pages_per_window, + seed = self.metagraph.hotkeys[self.uid] + ) + loader = await tplr.dataset.DatasetLoader.create( + batch_size = self.hparams.batch_size, + sequence_length = self.hparams.sequence_length, + pages_info = pages, + tokenizer = self.tokenizer + ) + tplr.logger.info(f"Pages: {[p[1] for p in pages]} for Window: {step_window}") + + # Accumulate gradient + start_time = time.time() + tplr.logger.info("Start accumulating...") + self.optimizer.zero_grad() + self.model.zero_grad() + total_loss = 0 + batch_tokens = 0 + + for i, batch in enumerate(loader): + input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) + labels = input_ids.clone() + labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) + + with torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): + outputs = self.model(input_ids=input_ids, labels=labels) + + total_loss += outputs.loss.item() + outputs.loss.backward() + + # Track tokens + batch_tokens += (labels != -100).sum().item() + + tplr.logger.info(f'loss: {outputs.loss.item()}') + if self.current_window != step_window: + tplr.logger.info('') + break + tplr.logger.info(f"Stopped accumulating: {i+1} batches with {(i+1) * self.hparams.batch_size * self.hparams.sequence_length} tokens") + + # Calculate processing metrics + duration = time.time() - start_time + self.batch_times.append(duration) + self.total_tokens_processed += batch_tokens + + # Log gradient metrics + grad_norms = [p.grad.norm().item() for p in self.model.parameters() if p.grad is not None] + weight_norms = [p.norm().item() for p in self.model.parameters()] + momentum_norms = [m.norm().item() for m in self.momentum.values()] + + # Enhanced wandb logging with all metrics + self.wandb.log({ + # Training metrics + "miner/loss": total_loss/(i+1), + "miner/tokens_per_sec": ((i+1) * self.hparams.batch_size * self.hparams.sequence_length)/duration, + "miner/batch_duration": duration, + "miner/total_tokens": self.total_tokens_processed, + "miner/batch_tokens": batch_tokens, + "miner/global_step": self.global_step, + + # Resource metrics + "miner/gpu_memory_allocated": torch.cuda.memory_allocated() / 1024**2, # MB + "miner/gpu_memory_cached": torch.cuda.memory_reserved() / 1024**2, # MB + + # Network metrics + "miner/active_peers": len(self.peers), + "miner/effective_batch_size": len(self.peers) * self.hparams.batch_size, + + # Optimization metrics + "miner/learning_rate": self.scheduler.get_last_lr()[0], + + # Gradient statistics as points + "miner/mean_grad_norm": sum(grad_norms) / len(grad_norms) if grad_norms else 0, + "miner/max_grad_norm": max(grad_norms) if grad_norms else 0, + "miner/min_grad_norm": min(grad_norms) if grad_norms else 0, + "miner/grad_norm_std": torch.tensor(grad_norms).std().item() if grad_norms else 0, + "miner/mean_weight_norm": sum(weight_norms) / len(weight_norms), + "miner/mean_momentum_norm": sum(momentum_norms) / len(momentum_norms), + }, step=self.global_step) + + # Log per-peer metrics + for peer_uid in self.peers: + self.wandb.log({ + f"miner/peer_stake/{peer_uid}": self.metagraph.S[peer_uid].item(), + }, step=self.global_step) + + # Reduce gradient using DeMo. + gradient = {} + xshapes = {} + totalks = {} + transmitted = {} + for n, p in self.model.named_parameters(): + # Step-Weight decay + p.data.mul_(1.0 - self.scheduler.get_last_lr()[0] * self.hparams.weight_decay) + # Momentum decay + self.momentum[n].mul_(self.hparams.momentum_decay) + # Add the grad to the momentum + self.momentum[n].add_(p.grad, alpha=self.scheduler.get_last_lr()[0]) + # Compress gradient + idxs, vals, xshape, totalk = self.compressor.compress( + self.transformer.encode(self.momentum[n]), + self.hparams.topk_compression + ) + # Estimate transmitted gradient + transmit_grad = self.transformer.decode( + self.compressor.decompress(p, idxs, vals, xshape, totalk) + ) + # Remove transmitted from delta + self.momentum[n].sub_(transmit_grad) + # Add to share_state + transmitted[n] = transmit_grad + gradient[n + 'idxs'] = idxs + gradient[n + 'vals'] = vals + xshapes[n] = xshape + totalks[n] = totalk + + # All-gather share state from peers + tplr.logger.info(f"Start gather: {self.peers}") + gather_result = await self.comms.gather( + state_dict=gradient, + my_uid=self.uid, + uids=self.peers, + window=step_window, + key='gradient', + timeout=5, + device=self.config.device, + local=False, + stale_retention=10 + ) + + # Decompress state and apply to grad. + for n, p in self.model.named_parameters(): + idxs_key = n + 'idxs' + vals_key = n + 'vals' + idxs = gather_result.state_dict.get(idxs_key) + vals = gather_result.state_dict.get(vals_key) + if idxs is not None and vals is not None: + # Ensure idx and val are lists of tensors + if not isinstance(idxs, (list, tuple)): + idxs = [idxs] + if not isinstance(vals, (list, tuple)): + vals = [vals] + + new_grad = self.transformer.decode( + self.compressor.batch_decompress( + p.to(self.config.device), + idxs, + vals, + xshapes[n], + totalks[n] ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - self.scheduler.last_epoch = self.global_step - 1 # Update scheduler - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied window state and updated global step to {self.global_step}.") - - # Download the page for the current window. - st = tplr.T() - pages = await tplr.dataset.DatasetLoader.next_pages( - offset = window, - n_pages = self.hparams.validator_window_eval_size, - seed = self.uid if not self.config.random else random.randint(0, 1000) - ) - random.shuffle( pages ) - dataset = await tplr.dataset.DatasetLoader.create( - batch_size = self.config.actual_batch_size, - sequence_length = self.hparams.sequence_length, - pages_info = pages, - tokenizer = self.hparams.tokenizer ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded training page: [light_steel_blue]{[p[1] for p in pages]}[/light_steel_blue] random = {self.config.random}") - - # Accumualte gradients on the model applied to the base state. - train_start = tplr.T() - self.model.zero_grad() - self.model.eval() - total_loss = 0.0 - full_steps = 0 - total_steps = 0 - exhausted_window = False - for batch in dataset: - total_steps += 1 - if random.random() < self.sample_rate and not exhausted_window: - full_steps += 1 - input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) - labels = input_ids.clone() - labels = torch.where(labels == self.hparams.tokenizer.pad_token_id, -100, labels) - with torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): # Enable autocasting - outputs = self.model(input_ids=input_ids, labels=labels) - total_loss += outputs.loss.item() - outputs.loss.backward() - if window != self.current_window and not self.config.baseline: - exhausted_window = True - continue - if self.hparams.grad_clip: - torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.hparams.grad_clip) - self.optimizer.step() - self.scheduler.step() - self.optimizer.zero_grad() - torch.cuda.empty_cache() - step_loss = total_loss/(full_steps+1) - train_duration = tplr.T() - train_start - tokens_per_step = self.hparams.sequence_length * self.config.actual_batch_size * (full_steps + 1) - tokens_per_second = tokens_per_step / train_duration - tplr.logger.info(f"{tplr.P(window, train_duration)} Accumulated gradients:") - tplr.logger.info(f"{tplr.P(window, train_duration)} \tTotal steps: [tan]{full_steps}/{total_steps}[/tan], Rate: [tan]{(full_steps/total_steps):.2f}[/tan], Target: [tan]{self.sample_rate:.2f}[/tan]") - tplr.logger.info(f"{tplr.P(window, train_duration)} \tTotal tokens: [tan]{tokens_per_step}[/tan], Tokens per second: [tan]{tokens_per_second:.2f}[/tan]") - tplr.logger.info(f"{tplr.P(window, train_duration)} \tLoss: [tan]{step_loss}[tan]") - if exhausted_window: - self.sample_rate = max(0.0001, self.sample_rate * 0.95) + # Set recomputed gathered gradient. + if p.grad is None: + p.grad = new_grad else: - self.sample_rate = min(1, self.sample_rate * 1.05) - - # Run for non-baseline nodes. - if not self.config.baseline: - # Upload the delta for the previous window. - st = tplr.T() - slice_metric = { - "batch_size": self.config.actual_batch_size, - "tokens_per_step": tokens_per_step, - "tokens_per_second": tokens_per_second, - "loss": step_loss, - "sample_rate": self.sample_rate, - "learning_rate": self.scheduler.get_last_lr()[0], - } - await tplr.upload_slice_for_window( - bucket = tplr.config.BUCKET_SECRETS["bucket_name"], - model = self.model, - window = window, - seed = window, - wallet = self.wallet, - compression = self.hparams.compression, - save_location = self.save_location, - key = 'delta', - global_step = self.global_step, - slice_metric = slice_metric, - ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Uploaded the delta.") - - # Apply the delta from the previous window. - st = tplr.T() - max_global_step, _ = await tplr.apply_slices_to_model( - model=self.model, - window=window - 1, - seed=window - 1, - compression=self.hparams.compression, - save_location=self.save_location, - key='delta' - ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - self.scheduler.last_epoch = self.global_step - 1 # Update scheduler - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied window delta and updated global step to {self.global_step}.") - - # Upload the state for the current window. - st = tplr.T() - await tplr.upload_slice_for_window( - bucket = tplr.config.BUCKET_SECRETS["bucket_name"], - model = self.model, - window = window + 1, - seed = window + 1, - wallet = self.wallet, - compression = self.hparams.compression, - save_location = self.save_location, - key = 'state', - global_step = self.global_step - ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Uploaded the state.") - - # Clean file history. - st = tplr.T() - await tplr.delete_files_before_window(window_max=window - self.hparams.max_history, save_location=self.save_location, key='state') - await tplr.delete_files_before_window(window_max=window - self.hparams.max_history, save_location=self.save_location, key='delta') - await tplr.delete_files_from_bucket_before_window( bucket = tplr.config.BUCKET_SECRETS["bucket_name"], window_max = window - self.hparams.max_history, key = 'state' ) - await tplr.delete_files_from_bucket_before_window( bucket = tplr.config.BUCKET_SECRETS["bucket_name"], window_max = window - self.hparams.max_history, key = 'delta' ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Cleaned file history.") - - # Wait until we are on a new window. - end_step = tplr.T() - while self.current_window == window: - await asyncio.sleep(0.1) - window_time_delta = self.window_time - end_step - window_delta_str = f"[red]{window_time_delta:.2f}[/red]" if window_time_delta < 0 else f"[green]+{window_time_delta:.2f}[/green]" - tplr.logger.info(f"{tplr.P(window, end_step - start_step)}[{window_delta_str}]: Finished step.") - wandb.log({ - "miner/loss": step_loss, - "miner/tokens_per_step": tokens_per_step, - "miner/tokens_per_second": tokens_per_second, - "miner/sample_rate": self.sample_rate, - "miner/utilization": train_duration / (end_step - start_step), - "miner/learning_rate": self.scheduler.get_last_lr()[0] - }, step=self.global_step) - - # Catch keyboard interrrupt. - except KeyboardInterrupt: - tplr.logger.info("Training interrupted by user. Stopping the run.") - self.stop_event.set() - await self.update_task - sys.exit(0) - - - # Catch unknown. - except Exception as e: - message = f"Exception during training loop: {escape(str(e))}" - tplr.logger.exception(message) - continue - finally: - # Wait for any pending checkpoint tasks to complete - if self.checkpoint_tasks: - tplr.logger.info(f"Waiting for {len(self.checkpoint_tasks)} checkpoint tasks to complete...") - await asyncio.gather(*self.checkpoint_tasks) - self.checkpoint_manager.cleanup() - tplr.logger.info("Miner shutdown complete.") - - - # Returns the slice window based on a block. - def block_to_window(self, block: int) -> int: - return int( block / self.hparams.window_length ) # floor - - # Returns the slice window based on a blotplr. - def window_to_seed(self, window: int) -> int: - return str( self.subtensor.get_block_hash( window * self.hparams.window_length ) ) - - # A listener thread which posts the block event - # when the chain announces a new blotplr. + p.grad.copy_(new_grad) + # Sign-SGD + p.grad.sign_() + else: + tplr.logger.info(f"Gradient data missing for parameter {n}, skipping.") + + # Apply optimizer step + tplr.logger.info("Finish and step.") + self.optimizer.step() + self.scheduler.step() + self.global_step += 1 + self.window_step += 1 + tplr.logger.info(f"Total optimization steps: {self.global_step}") + + # Wait for next window + tplr.logger.info("Wait for next window...") + while self.current_window == step_window: + await asyncio.sleep(0.1) + self.window_step = 0 + + # Listens for new blocks and sets self.current_block and self.current_window def block_listener(self, loop): def handler(event, _u, _s): self.current_block = int(event['header']['number']) - loop.call_soon_threadsafe(self.new_block_event.set) - if self.block_to_window(self.current_block) != self.current_window: - self.window_seeds[ self.block_to_window(self.current_block) ] = self.window_to_seed( self.block_to_window(self.current_block) ) - self.current_window = self.block_to_window(self.current_block) - self.window_duration = tplr.T() - self.window_time if hasattr(self, 'window_time') else 0 - self.window_time = tplr.T() - loop.call_soon_threadsafe(self.new_window_event.set) - tplr.logger.info(f"{tplr.P(self.current_window, self.window_duration)} New Window.") - # Run listener with retry. + if int(self.current_block / self.hparams.blocks_per_window) != self.current_window: + self.current_window = int(self.current_block / self.hparams.blocks_per_window) while not self.stop_event.is_set(): try: bt.subtensor(config=self.config).substrate.subscribe_block_headers(handler) break - except Exception as e: - tplr.logger.error(f"Failed to subscribe to block headers: {e}.\nRetrying in 1 seconds...") + except Exception: time.sleep(1) +# Start miner/validator. if __name__ == "__main__": asyncio.run(Miner().run()) diff --git a/neurons/validator.py b/neurons/validator.py index a7001ed..8d1710a 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -16,847 +16,454 @@ # DEALINGS IN THE SOFTWARE. # fmt: off -# Global imports. -import argparse -import asyncio -import bittensor as bt -import numpy as np -import os -import random +# Standard library import sys -import threading import time +import random +import asyncio +import argparse +import threading + +# Third party import torch -from tqdm import tqdm +import numpy as np +import bittensor as bt +from torch.optim import SGD from transformers import LlamaForCausalLM -import wandb -import wandb.plot -from asyncio import TimeoutError -from functools import partial -import tempfile +from torch.optim.lr_scheduler import ( + CosineAnnealingWarmRestarts, + LinearLR, + SequentialLR, +) -# Local imports. -import templar as tplr +# Local +import tplr # GPU optimizations. -torch.backends.cudnn.benchmark = True +torch.manual_seed(42) +torch.cuda.manual_seed_all(42) +np.random.seed(42) +random.seed(42) +torch.backends.cudnn.deterministic = True +torch.backends.cudnn.benchmark = False torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True class Validator: - @staticmethod def config(): parser = argparse.ArgumentParser(description='Validator script') - parser.add_argument('--project', type=str, default='templar', help='Optional wandb project name') - parser.add_argument('--netuid', type=int, default=3, help='Bittensor network UID.') - parser.add_argument('--actual_batch_size', type=int, default=8, help='Training batch size per accumulation.') - parser.add_argument('--device', type=str, default='cuda', help='Device to use for training (e.g., cpu or cuda)') - parser.add_argument('--use_wandb', action='store_true', help='Use Weights and Biases for logging') + parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') + parser.add_argument('--project', type=str, default='templar', help='Wandb project.') + parser.add_argument('--device', type=str, default='cuda', help='Device to use for training') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--trace', action='store_true', help='Enable trace logging') - parser.add_argument('--sync_state', action='store_true', help='Syncs the model state by pulling from the history.') - parser.add_argument('--test', action='store_true', help='Run on test network') - parser.add_argument('--local', action='store_true', help='Run on local network') - parser.add_argument('--no_autoupdate', action='store_true', help='Disable automatic updates') - parser.add_argument("--process_name", type=str, help="The name of the PM2 process") - parser.add_argument('--checkpoint_path', type=str, default=None, help='Path to save/load the checkpoint. If None, the path is set to checkpoint-V.pth.') - parser.add_argument('--save-location', type=str, default=None, help='Directory to save/load slice files') - bt.wallet.add_args(parser) + parser.add_argument('--use_wandb', action='store_true', help='Use Weights and Biases for logging') + parser.add_argument('--peers', type=int, nargs='+', default=[], help='List of UIDs to peer with') bt.subtensor.add_args(parser) + bt.logging.add_args(parser) + bt.wallet.add_args(parser) config = bt.config(parser) - if config.test: - config.subtensor.network = 'test' - config.subtensor.chain_endpoint = 'wss://test.finney.opentensor.ai:443/' - elif config.local: - config.subtensor.network = 'local' - config.subtensor.chain_endpoint = 'ws://127.0.0.1:9944' if config.debug: tplr.debug() if config.trace: tplr.trace() - if not config.no_autoupdate: - autoupdater = tplr.AutoUpdate() - autoupdater.daemon = True # Ensure thread exits when main program exits - autoupdater.start() return config - + def __init__(self): - # Init config. + tplr.logger.debug("Starting initialization...") + + # Init config and load hparams self.config = Validator.config() - tplr.logger.info('\n' + '-' * 40 + ' Config ' + '-' * 40) - tplr.logger.info(self.config) - - # Init bittensor objects. + self.hparams = tplr.load_hparams() + + # Init bittensor objects self.wallet = bt.wallet(config=self.config) self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(netuid=self.config.netuid) + self.metagraph = self.subtensor.metagraph(self.config.netuid) if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: - tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]. You need to register first with: [blue]`btcli subnet register`[/blue]\n') + tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]') sys.exit() self.uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) - self.chain_manager = tplr.chain.ChainManager( - subtensor=self.subtensor, wallet=self.wallet, netuid=self.config.netuid + + # Init model with hparams config + self.model = LlamaForCausalLM(self.hparams.model_config) + self.model.to(self.config.device) + self.tokenizer = self.hparams.tokenizer + + # Init compression + self.transformer = tplr.compress.TransformDCT( + self.model, + target_chunk=self.hparams.target_chunk ) - tplr.logger.info('\n' + '-' * 40 + ' Objects ' + '-' * 40) - tplr.logger.info(f'\nWallet: {self.wallet}\nSubtensor: {self.subtensor}\nMetagraph: {self.metagraph}\nUID: {self.uid}') - - # Init bucket. - try: - tplr.logger.debug(f'bucket_name: {tplr.config.BUCKET_SECRETS["bucket_name"]}') - commitment = self.chain_manager.get_commitment(self.uid) - - # Convert Bucket object back to concatenated string format for comparison - commitment_str = commitment.name + commitment.access_key_id + commitment.secret_access_key - - current_bucket = ( - tplr.config.BUCKET_SECRETS["bucket_name"] + - tplr.config.BUCKET_SECRETS["read"]["access_key_id"] + - tplr.config.BUCKET_SECRETS["read"]["secret_access_key"] + self.compressor = tplr.compress.CompressDCT() + + # Init optimizer and momentum + self.optimizer = SGD(self.model.parameters(), lr=self.hparams.learning_rate) + self.momentum = {} + self.xshapes = {} + self.totalks = {} + for n, p in self.model.named_parameters(): + self.momentum[n] = torch.zeros_like(p) + _, _, xshape, totalk = self.compressor.compress( + self.transformer.encode(self.momentum[n]), + self.hparams.topk_compression ) - tplr.logger.debug(f'Comparing:\nCommitment: {commitment_str}\nCurrent: {current_bucket}') - - if current_bucket != commitment_str: - raise ValueError("Bucket commitment data does not match.") - - except Exception as e: - tplr.logger.error(f"Commitment error: {str(e)}") - tplr.commit(self.subtensor, self.wallet, self.config.netuid) - - # Init Wandb. - # Ensure the wandb directory exists - wandb_dir = os.path.join(os.getcwd(), 'wandb') - os.makedirs(wandb_dir, exist_ok=True) - - # Define the run ID file path inside the wandb directory - run_id_file = os.path.join(wandb_dir, f"wandb_run_id_V{self.uid}_{tplr.__version__}.txt") - - # Attempt to read the existing run ID - if os.path.exists(run_id_file): - with open(run_id_file, 'r') as f: - run_id = f.read().strip() - tplr.logger.info(f"Resuming WandB run with id {run_id}") - else: - run_id = None - tplr.logger.info("Starting a new WandB run.") + self.xshapes[n] = xshape + self.totalks[n] = totalk - # Initialize WandB - self.wandb = tplr.initialize_wandb( - run_prefix='V', - uid=self.uid, - config=self.config, - group='validator', - job_type='validation' + # Set up scheduler setup + warmup_scheduler = LinearLR( + self.optimizer, + total_iters=250 ) - - - # Set checkpoint path - if self.config.checkpoint_path is None: - # Default path if none provided - self.checkpoint_path = f"checkpoints/checkpoint-V{self.uid}.pth" - else: - self.checkpoint_path = self.config.checkpoint_path - - # Create checkpoint directory if it doesn't exist - os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) - - # Retrieve bucket info for all neurons - self.buckets = tplr.get_all_buckets( - netuid=self.config.netuid, - metagraph=self.metagraph, - config= self.config + cosine_scheduler = CosineAnnealingWarmRestarts( + self.optimizer, + T_0=10000, + T_mult=1, + eta_min=self.hparams.learning_rate * 0.1 ) - - # Init model. - tplr.logger.info('\n' + '-' * 40 + ' Hparams ' + '-' * 40) - self.hparams = tplr.load_hparams() - torch.manual_seed(42) - np.random.seed(42) - random.seed(42) - self.model = LlamaForCausalLM(config=self.hparams.model_config) - self.model.to(self.config.device) - self.model.eval() - - self.optimizer = torch.optim.AdamW( - self.model.parameters(), - lr=self.hparams.learning_rate*self.hparams.validator_learning_rate_scale_factor, - betas=(self.hparams.optimizer_beta1, self.hparams.optimizer_beta2), - weight_decay=self.hparams.optimizer_weight_decay, - foreach=True + self.scheduler = SequentialLR( + self.optimizer, + schedulers=[warmup_scheduler, cosine_scheduler], + milestones=[250] ) - self.scheduler = tplr.get_wsd_scheduler( - optimizer=self.optimizer, - num_warmup_steps=self.hparams.num_warmup_steps, - num_stable_steps=self.hparams.num_stable_steps, - num_decay_steps=self.hparams.num_decay_steps, - ) - - # Initialize checkpoint manager - self.checkpoint_manager = tplr.CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, + # Init comms with required chain management args + self.comms = tplr.comms.Comms( wallet=self.wallet, - device=self.config.device, - ) - - # Load initial checkpoint - self.global_step = asyncio.run( - self.checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets, - optimizer=self.optimizer, - scheduler=self.scheduler, - is_validator=True, - hparams=self.hparams - ) + save_location='/tmp', + key_prefix='model', + config=self.config, + netuid=self.config.netuid, + metagraph=self.metagraph, + hparams=self.hparams, ) - self.last_window = 0 - self.optimal_pages_per_step = 4 - self.current_block = self.subtensor.block - self.current_window = self.block_to_window( self.current_block ) - self.window_seeds = {self.current_window: self.window_to_seed( self.current_window) } - self.block_event = asyncio.Event() - self.new_window_event = asyncio.Event() - self.stop_event = asyncio.Event() - self.step_scores = torch.zeros( 256, dtype = torch.float32 ) - self.step_loss_scores = torch.zeros( 256, dtype = torch.float32 ) - self.scores = torch.zeros( 256, dtype = torch.float32 ) - self.weights = torch.zeros( 256, dtype = torch.float32 ) - self.sample_rate = 1.0 - self.save_location = self.config.save_location - if self.save_location is None: - # Default to system temp dir with unique neuron directory - self.save_location = os.path.join( - tempfile.gettempdir(), f"neuron_{self.wallet.hotkey.ss58_address}" - ) + # Init peers + if not self.config.peers: + self.peers = self.comms.peers + tplr.logger.info(f'Filtered peers with buckets: {self.peers}') else: - # Append neuron-specific directory to save_location - self.save_location = os.path.join( - self.config.save_location, f"neuron_{self.wallet.hotkey.ss58_address}" - ) - - # Create the directory if it doesn't exist - os.makedirs(self.save_location, exist_ok=True) - self.checkpoint_tasks = set() - print ( self.hparams ) - - # Configuration for weight setting - self.weight_setting_config = { - 'timeout': 60, # seconds - 'max_retries': 3, - 'retry_delay': 5, - 'health_check_interval': 300 # 5 minutes - } - - # At the beginning of the Validator class, add a new attribute to track checkpoint tasks - self.checkpoint_tasks = set() # Track checkpoint tasks - self.checkpoint_lock = asyncio.Lock() # Add lock for thread safety - - async def update(self): - """Continuously updates the global state by polling every 10 minutes.""" - await asyncio.sleep(600) # Initial sleep before starting updates - while not self.stop_event.is_set(): - st = tplr.T() - await self.perform_update() - tplr.logger.info(f"{tplr.P(self.current_window, tplr.T() - st)} Updated global state.") - await asyncio.sleep(600) - - async def perform_update(self): - """Updates subtensor connection, metagraph, hyperparameters, and buckets.""" - self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(self.config.netuid) - - buckets = tplr.get_all_commitments( - substrate=self.subtensor.substrate, - netuid=self.config.netuid, - metagraph=self.metagraph + self.peers = self.config.peers + + # Init state params + self.stop_event = asyncio.Event() + self.current_block = self.subtensor.block + self.current_window = int(self.current_block / self.hparams.blocks_per_window) + self.sync_window = self.current_window + + # Init scores + self.scores = torch.zeros(self.metagraph.n, dtype=torch.float32) + self.moving_avg_scores = torch.zeros(self.metagraph.n, dtype=torch.float32) # Add moving average tracking + self.ma_alpha = 0.95 # Moving average decay factor + + # Add step tracking + self.global_step = 0 + self.window_step = 0 + self.eval_count = 0 # Track number of evaluations + + # Initialize WandB + self.wandb = tplr.initialize_wandb( + run_prefix='V', + uid=self.uid, + config=self.config, + group='validator', + job_type='validation' ) - self.buckets = [] - for uid in self.metagraph.uids: - bucket = buckets.get(uid) - if isinstance(bucket, bytes): - bucket = bucket.decode('utf-8') - if bucket is not None: - tplr.logger.debug(f"UID {uid}: Valid bucket found: {bucket}") - self.buckets.append(bucket) - else: - tplr.logger.debug(f"UID {uid}: Invalid or missing bucket: {bucket}") - self.buckets.append(None) - - async def load_checkpoint_background(self): - """Handles checkpoint loading in the background.""" - try: - tplr.logger.info(f"Loading checkpoint at step {self.global_step}") - - # Load the checkpoint into a temporary model - temp_model = LlamaForCausalLM(config=self.hparams.model_config).to(self.config.device) - temp_checkpoint_manager = tplr.CheckpointManager( - model=temp_model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device=self.config.device, - ) - - # Load the checkpoint from the highest stake - await temp_checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets - ) - - # Safely update the main model's parameters - for param, temp_param in zip(self.model.parameters(), temp_model.parameters()): - param.data.copy_(temp_param.data) - - tplr.logger.info(f"Checkpoint loaded at step {self.global_step}") - - # Clean up the temporary model to free memory - del temp_model, temp_checkpoint_manager - torch.cuda.empty_cache() - - except Exception as e: - tplr.logger.error(f"Error loading checkpoint in background: {str(e)}") async def run(self): - # Main loop. - self.loop = asyncio.get_running_loop() - self.update_task = asyncio.create_task(self.update()) - self.listener = threading.Thread(target=self.block_listener, args=(self.loop,), daemon=True).start() - self.checkpoint_tasks = set() - - # Optionally sync the model state by pulling model states from the history. - if self.config.sync_state: - st = tplr.T() - history_windows = [ self.current_window - i for i in range (self.hparams.max_history) ] - state_slices = await tplr.download_slices_for_buckets_and_windows( - buckets=[b for b in self.buckets if b is not None], - windows = history_windows, - key = 'state', - save_location=self.save_location - ) - for window in tqdm(history_windows, desc="Syncing state"): - max_global_step, _ = await tplr.apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - save_location=self.save_location, - key='state', + # Try to load latest checkpoint + validator_uid, stake = self.comms.get_highest_stake_validator() + if stake > 0: + try: + state_dict = await self.comms.get( + uid=str(validator_uid), + window=self.current_window, + key='checkpoint', + timeout=240, + local=False ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied historical state and updated global step to {self.global_step}.") - torch.cuda.empty_cache() - - try: - # Run validation. - while True: + if state_dict is not None: + self.model.load_state_dict(state_dict) + tplr.logger.info(f"Loaded checkpoint from validator {validator_uid} at window {self.current_window}") + else: + tplr.logger.info("No checkpoint found, starting from scratch") + except Exception as e: + tplr.logger.warning(f"Failed to load checkpoint: {e}") + else: + tplr.logger.info("No active validators found, starting from scratch") + # Start block listener + self.loop = asyncio.get_running_loop() + self.listener = threading.Thread( + target=self.block_listener, + args=(self.loop,), + daemon=True + ).start() + + while True: + step_window = self.current_window + # Wait for validator offset + while self.sync_window >= (self.current_window - self.hparams.validator_offset): + tplr.logger.info(f'Waiting for validator window offset, synced: {self.sync_window}, current:{self.current_window}, offset:{self.hparams.validator_offset}') + await asyncio.sleep(12) + + # Check if checkpointing is needed (every 500 windows) + if self.current_window % 500 == 0: + tplr.logger.info(f'Creating checkpoint at window {self.current_window}') + try: - # Get the window we are evalling. - tplr.logger.info('[bold]' + '\n' + '-' * 40 + f' Step: {self.global_step} ' + '-' * 40) - gs_start = tplr.T() - self.global_step += 1 - offset = 2 - window = self.current_window - offset - - - # Upload checkpoint every 500 steps - if self.global_step % 500 == 0: - # Create background task for checkpoint operations - checkpoint_task = asyncio.create_task( - self.save_checkpoint_background( - global_step=self.global_step, - block_number=self.current_block, - scores=self.scores.clone(), # Clone to avoid race conditions - weights=self.weights.clone() # Clone to avoid race conditions - ) - ) - self.checkpoint_tasks.add(checkpoint_task) - checkpoint_task.add_done_callback(self.checkpoint_tasks.discard) - - # Download the state for the eval window. - st = tplr.T() - valid_buckets = [b for b in self.buckets if b is not None] - - if not valid_buckets: - tplr.logger.info(f"No valid buckets to download state slices for window {window}") - # Wait for the next window - while self.current_window - offset == window: - await asyncio.sleep(0.1) # Keep waiting until the window changes - - - state_slices = await tplr.download_slices_for_buckets_and_windows( - buckets=valid_buckets, - windows=[window], - key='state', - save_location=self.save_location + # Upload the model state directly using put + await self.comms.put( + state_dict=self.model.state_dict(), + uid=self.uid, + window=self.current_window, + key='checkpoint', + local=False ) - n_state_slices = len(state_slices[window]) if window in state_slices else 0 - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded {n_state_slices} window states.") - - # Download the delta for the eval window. - st = tplr.T() - eval_slices = await tplr.download_slices_for_buckets_and_windows( - buckets = self.buckets, - windows = [ window ], - key = 'delta', - save_location=self.save_location - ) - n_eval_slices = len(eval_slices[ window ]) if window in eval_slices else 0 - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded {n_eval_slices} window deltas.") - # Collect UIDs of miners who submitted slices - submitted_uids = set() - if window in eval_slices: - for slice_info in eval_slices[window]: - if getattr(slice_info, 'version', None) == tplr.__version__: - try: - uid = self.metagraph.hotkeys.index(slice_info.hotkey) - submitted_uids.add(uid) - except ValueError: - tplr.logger.warning(f"Hotkey {slice_info.hotkey} not found in metagraph") - if n_eval_slices == 0: - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: No slices to eval, continue ...") - while self.current_window - offset == window: - await asyncio.sleep(0.1) # Wait for next window. - continue - - # Applied the model state for the eval window. - st = tplr.T() - max_global_step, _ = await tplr.apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - save_location=self.save_location, - key='state', + tplr.logger.info(f"Successfully created checkpoint at window {self.current_window}") + except Exception as e: + tplr.logger.error(f"Failed to create checkpoint: {e}") + + # Log checkpoint creation + if self.current_window % 500 == 0: + self.wandb.log({ + "checkpoint_window": self.current_window, + "global_step": self.global_step, + }, step=self.global_step) + + # Catch up to current - validator_offset + while self.sync_window < (self.current_window - self.hparams.validator_offset): + self.sync_window += 1 + tplr.logger.info(f'Syncing window: {self.sync_window} current: {self.current_window}') + + # Gather gradients from this window + step_grads = await self.comms.gather( + state_dict={}, + my_uid=self.uid, + uids=self.peers, + window=self.sync_window, + key='gradient', + timeout=5, + device=self.config.device, + local=False + ) + # Log gradient stats + tplr.logger.info(f"Gradient stats - Window: {self.sync_window}") + # Check if any gradients were gathered + if not step_grads == 0: + tplr.logger.info("No gradients received, waiting for next window.") + continue + + # Decompress state and apply to gradients + for n, p in self.model.named_parameters(): + new_grad = self.transformer.decode( + self.compressor.batch_decompress( + p.to(self.config.device), + step_grads.state_dict[n + 'idxs'], + step_grads.state_dict[n + 'vals'], + self.xshapes[n], self.totalks[n] + ) ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied window state and updated global step to {self.global_step}.") - - # Obtain the indicies for the eval window. - st = tplr.T() - indices = await tplr.get_indices_for_window( - model = self.model, - seed = window, - compression = self.hparams.compression - ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Obtained window indices.") - - - # Attain the UID of this slice. - st = tplr.T() - valid_eval_slices = [s for s in eval_slices[window] if getattr(s, 'version', None) == tplr.__version__] - if not valid_eval_slices: - tplr.logger.warning(f"{tplr.P(window, tplr.T() - st)}: No valid slices with matching version {tplr.__version__}, continuing...") - while self.current_window - offset == window: - await asyncio.sleep(0.1) # Wait for next window. - continue - eval_slice_info = random.choice(valid_eval_slices) - try: - eval_uid = self.metagraph.hotkeys.index(eval_slice_info.hotkey) - except ValueError: - tplr.logger.warning(f"{tplr.P(window, tplr.T() - st)}: {eval_slice_info.hotkey} not found in metagraph") - continue - eval_slice_data = await tplr.get_slices(eval_slice_info.temp_file, self.model.device) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Loaded window slices for uid: [dark_sea_green]{eval_uid}[/dark_sea_green].") - - # Download the eval page for this uid. - st = tplr.T() - eval_pages = await tplr.dataset.DatasetLoader.next_pages( - offset = window, - n_pages = self.hparams.validator_window_eval_size, - seed = eval_uid - ) - random.shuffle(eval_pages) - eval_dataset = await tplr.dataset.DatasetLoader.create( - batch_size = self.config.actual_batch_size, - sequence_length = self.hparams.sequence_length, - pages_info = eval_pages, - tokenizer = self.hparams.tokenizer - ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded eval pages: [light_steel_blue]{[p[1] for p in eval_pages]}[/light_steel_blue].") - - - # Accumulate gradients from this page. - eval_start = tplr.T() - self.model.zero_grad() - total_loss = 0.0 - step_loss_after = 0.0 - full_steps = 0 - total_steps = 0 - exhausted_window = False - with torch.enable_grad(): - for idx, batch in enumerate(eval_dataset): - total_steps += 1 - if random.random() < self.sample_rate and not exhausted_window: - full_steps += 1 - input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) - labels = input_ids.clone() - labels = torch.where(labels == self.hparams.tokenizer.pad_token_id, -100, labels) - - # Store original parameters - original_params = {} - for name_i, param_i in self.model.named_parameters(): - original_params[name_i] = param_i.data.clone() - - # Apply miner's slice data to the model - for name_i, param_i in self.model.named_parameters(): - if name_i not in indices or name_i not in eval_slice_data: - continue - - idxs_i = indices[name_i].to(self.model.device) - slice_i = eval_slice_data[name_i].view(-1).to(self.model.device) - - # Update the parameter data at the specified indices - param_i.data.view(-1)[idxs_i] = slice_i - - # Perform forward pass with updated model (no gradients needed) - with torch.no_grad(), torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): - outputs_after = self.model(input_ids=input_ids, labels=labels) - step_loss_after += outputs_after.loss.item() - - # Restore original parameters - for name_i, param_i in self.model.named_parameters(): - param_i.data.copy_(original_params[name_i]) - - # Perform forward pass and compute loss with gradients (before applying delta) - with torch.enable_grad(), torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): - outputs_before = self.model(input_ids=input_ids, labels=labels) - loss = outputs_before.loss - loss.backward() - total_loss += loss.item() - - if self.current_window - offset != window: - exhausted_window = True - continue - - self.optimizer.step() - self.scheduler.step() - step_loss = total_loss / (full_steps + 1) - step_loss_after = step_loss_after / (full_steps + 1) - - eval_duration = tplr.T() - eval_start - tokens_per_step = self.hparams.sequence_length * self.config.actual_batch_size * (full_steps + 1) - tokens_per_second = tokens_per_step / eval_duration - - tplr.logger.info(f"{tplr.P(window, eval_duration)}: Accumulated gradients:") - tplr.logger.info(f"{tplr.P(window, eval_duration)}: \tTotal steps: [tan]{full_steps}/{total_steps}[/tan], Rate: [tan]{(full_steps/total_steps):.2f}[/tan], Target: [tan]{self.sample_rate:.2f}[/tan]") - tplr.logger.info(f"{tplr.P(window, eval_duration)}: \tTotal tokens: [tan]{tokens_per_step}[/tan], Tokens per second: [tan]{tokens_per_second:.2f}[/tan]") - tplr.logger.info(f"{tplr.P(window, eval_duration)}: \tLoss before applying delta: [tan]{step_loss:.4f}[/tan]") - tplr.logger.info(f"{tplr.P(window, eval_duration)}: \tLoss after applying delta: [tan]{step_loss_after:.4f}[/tan]") - - if exhausted_window: - self.sample_rate = max(0.0001, self.sample_rate * 0.95) + # Set recomputed gathered gradient + if p.grad is None: + p.grad = new_grad else: - self.sample_rate = min(1, self.sample_rate * 1.05) - - # Compute the score for this slice. - st = tplr.T() - - # Collect all delta_i and grad_i into larger vectors - all_delta = [] - all_grad = [] - - for name_i, param_i in self.model.named_parameters(): - if param_i.grad is None: - continue + p.grad.copy_(new_grad) + p.grad.sign_() - if name_i not in indices or name_i not in eval_slice_data: - continue - - idxs_i = indices[name_i].to(self.model.device) - grad_i = param_i.grad.view(-1)[idxs_i].to(self.model.device) - slice_i = eval_slice_data[name_i].view(-1).to(self.model.device) - theta_i = param_i.data.view(-1)[idxs_i] - delta_i = theta_i - slice_i - - all_delta.append(delta_i) - all_grad.append(grad_i) - - if len(all_delta) > 0: - # Concatenate all parts - all_delta = torch.cat(all_delta) - all_grad = torch.cat(all_grad) - - # Compute cosine similarity between miner's delta and validator's gradients - cosine_similarity = torch.nn.functional.cosine_similarity(all_delta, all_grad, dim=0).item() - else: - tplr.logger.warning("No valid parameter tensors found - setting cosine similarity to 0.0") - cosine_similarity = 0.0 - - # Set initial score to 0.0 - score = 0.0 - - # Check if cosine similarity is greater than zero - if cosine_similarity > 0.0: - # Base score from cosine similarity - base_score = 0.0 - - # Compute the loss difference (percentage) - loss_difference = step_loss_after - step_loss # Positive if miner's loss is worse - percentage_loss_difference = loss_difference / step_loss # Fractional change - - if loss_difference < 0: # Miner improved the loss - # Miner improved the loss, add to base score - score = base_score + (-loss_difference * 100) # Negative because loss decreased - elif percentage_loss_difference <= 0.25: - # Loss did not improve but is not worse by more than 25% - score = base_score # Only base score - else: - # Loss is worse by more than 25%, zero out their moving average score - self.scores[eval_uid] = 0.0 - score = 0.0 - else: - tplr.logger.info(f"Cosine similarity ({cosine_similarity:.4f}) not positive. Setting score to 0.0") - score = 0.0 - tplr.logger.info(f"Cosine similarity: [bold dark_sea_green]{cosine_similarity:.4f}[/bold dark_sea_green]") - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Computed score for miner {eval_uid}: [bold dark_sea_green]{score:.4f}[/bold dark_sea_green]") - self.optimizer.zero_grad() - - # Update the score for the evaluated miner - st = tplr.T() - - # First update the step score - self.step_scores[eval_uid] = score - - # Then update the moving average score using EMA - self.scores[eval_uid] = ( - self.hparams.validator_moving_alpha * self.step_scores[eval_uid] + - (1 - self.hparams.validator_moving_alpha) * self.scores[eval_uid] + # Apply the optimizer step + self.optimizer.step() + self.scheduler.step() + + self.wandb.log({"lr": self.scheduler.get_last_lr()[0]}, step=self.global_step) + + # Get a random peer to eval on their gradient at self.sync_window + 1 + eval_uid = random.choice(self.peers) + # Get the pages for the window infront of the current sync window + pages = await tplr.dataset.DatasetLoader.next_pages( + offset=self.sync_window + 1, + n_pages=self.hparams.pages_per_window, + seed=eval_uid + ) + loader = await tplr.dataset.DatasetLoader.create( + batch_size=self.hparams.batch_size, + sequence_length=self.hparams.sequence_length, + pages_info=pages, + tokenizer=self.tokenizer + ) + tplr.logger.info(f'Evaluating uid: {eval_uid} on window: {self.sync_window + 1} with state from: {self.sync_window} and pages: {[p[1] for p in pages]}') + + # Compute and log loss before gradient application + loss_before = 0 + n_tokens = 0 + for i, batch in enumerate(loader): + input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) + labels = input_ids.clone() + labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) + loss_before += self.model(input_ids=input_ids, labels=labels).loss.item() + n_tokens += (labels != -100).sum().item() + + loss_before_per_token = loss_before / n_tokens if n_tokens > 0 else 0 + tplr.logger.info(f'Computed total loss before: {loss_before} ({loss_before_per_token:.4f} per token)') + + # Get the gradients from this miner on this window + eval_grad = await self.comms.get( + uid=eval_uid, + window=self.sync_window + 1, + key='gradient', + timeout=5, + local=False, + stale_retention=10 + ) + if eval_grad is None: + score = 0 + tplr.logger.info(f'Miner with uid: {eval_uid} has no gradient for window: {self.sync_window + 1}') + continue + + # Apply grad to model which is at state sync_window + for n, p in self.model.named_parameters(): + # Decompress their gradient + decompressed_grad = self.transformer.decode( + self.compressor.decompress( + p.to(self.config.device), + eval_grad[n + 'idxs'].to(self.config.device), + eval_grad[n + 'vals'].to(self.config.device), + self.xshapes[n], self.totalks[n], ) - - # Apply decay to miners who did not submit slices - all_uids = set(self.metagraph.uids.tolist()) - non_submitted_uids = all_uids - submitted_uids - decay_factor = self.hparams.validator_non_submission_decay - for uid in non_submitted_uids: - self.scores[uid] *= decay_factor - - # Prepare moving scores for normalization - moving_scores_tensor = self.scores.clone() - # Set negative moving scores to 0 - moving_scores_tensor[moving_scores_tensor < 0] = 0 - - # Normalize the positive moving average scores to get weights - positive_scores = moving_scores_tensor - sum_positive_scores = positive_scores.sum() - - if sum_positive_scores > 0: - self.weights = positive_scores / sum_positive_scores - else: - # Handle the case where all scores are zero or negative - self.weights = positive_scores # All zeros in this case - - # Log updated scores and weights - valid_score_indices = torch.nonzero(self.scores > 0).squeeze().view(-1) - for uid_i in valid_score_indices: - uid = uid_i.item() - moving_score = self.scores[uid].item() - weight = self.weights[uid].item() - step_score = self.step_scores[uid].item() - tplr.logger.info( - f"\tuid: [dark_sea_green]{uid}[/dark_sea_green], " - f"step_score: [dark_sea_green]{step_score:.3f}[/dark_sea_green], " - f"moving_score: [dark_sea_green]{moving_score:.3f}[/dark_sea_green], " - f"weight: [dark_sea_green]{weight:.3f}[/dark_sea_green]" - ) - - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Updated scores and weights.") - - # Apply all deltas to the model state. - st = tplr.T() - max_global_step, window_metric = await tplr.apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - save_location=self.save_location, - key='delta', + ) + # Apply this grad to the param of the model using the learning rate of the scheduler + p.data.sub_(decompressed_grad, alpha=self.scheduler.get_last_lr()[0]) + + # Compute loss after gradient application + loss_after = 0 + n_tokens = 0 + for i, batch in enumerate(loader): + input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) + labels = input_ids.clone() + labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) + loss_after += self.model(input_ids=input_ids, labels=labels).loss.item() + n_tokens += (labels != -100).sum().item() + if self.current_window != step_window: + tplr.logger.info('') + break + + loss_after_per_token = loss_after / n_tokens if n_tokens > 0 else 0 + tplr.logger.info(f'Computed total loss after: {loss_after} ({loss_after_per_token:.4f} per token)') + + # Remove gradient from the model + for n, p in self.model.named_parameters(): + # Decompress their gradient + decompressed_grad = self.transformer.decode( + self.compressor.decompress( + p.to(self.config.device), + eval_grad[n + 'idxs'].to(self.config.device), + eval_grad[n + 'vals'].to(self.config.device), + self.xshapes[n], self.totalks[n], ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied window delta and updated global step to {self.global_step}.") - - # Clean local and remote space from old slices. - st = tplr.T() - await tplr.delete_files_before_window(window_max=window - self.hparams.max_history, save_location=self.save_location, key='state') - await tplr.delete_files_before_window(window_max=window - self.hparams.max_history, save_location=self.save_location, key='delta') - await tplr.delete_files_from_bucket_before_window(bucket=tplr.config.BUCKET_SECRETS["bucket_name"], window_max=window - self.hparams.max_history, key='state') - await tplr.delete_files_from_bucket_before_window(bucket=tplr.config.BUCKET_SECRETS["bucket_name"], window_max=window - self.hparams.max_history, key='delta') - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Cleaned file history.") - - # Finish step. - gs_end = tplr.T() - while self.current_window - offset == window: - await asyncio.sleep(0.1) - window_time_delta = self.window_time - gs_end - window_delta_str = f"[red]{window_time_delta:.2f}[/red]" if window_time_delta < 0 else f"[green]+{window_time_delta:.2f}[/green]" - tplr.logger.info(f"{tplr.P(window, gs_end - gs_start)}[{window_delta_str}]: Finished step.") - - # Log main metrics - wandb.log({ - "validator/loss": step_loss, - "validator/tokens_per_step": sum(slice_metric['tokens_per_step'] for _, slice_metric in window_metric.items()), - "validator/tokens_per_second": sum(slice_metric['tokens_per_second'] for _, slice_metric in window_metric.items()), - "validator/sample_rate": self.sample_rate, - "validator/utilization": eval_duration / (gs_end - gs_start), - "validator/global_batch_size": sum(slice_metric['batch_size'] for _, slice_metric in window_metric.items()), + ) + # Apply this grad to the param of the model using the learning rate of the scheduler + p.data.add_(decompressed_grad, alpha=self.scheduler.get_last_lr()[0]) + + # Compute improvement metrics + loss_improvement = loss_before - loss_after + improvement_percentage = ((loss_before - loss_after) / loss_before * 100) if loss_before != 0 else 0 + + # Compute score + score = loss_before - loss_after + tplr.logger.info(f'score: {score}') + + # Log comprehensive metrics + self.wandb.log({ + "validator/loss_before": loss_before_per_token, + "validator/loss_after": loss_after_per_token, + "validator/loss_improvement": loss_improvement, + "validator/improvement_percentage": improvement_percentage, + "validator/eval_count": self.eval_count, + "validator/tokens_evaluated": n_tokens, + "validator/learning_rate": self.scheduler.get_last_lr()[0], + "validator/window": self.current_window, + "validator/global_step": self.global_step, + "validator/current_score": score, + }, step=self.global_step) + + # Update counters + self.global_step += 1 + self.eval_count += 1 + + # Set weights if needed + if self.sync_window % self.hparams.windows_per_weights == 0: + # Update scores with new score + self.scores[eval_uid] = self.hparams.scores_alpha * score + (1 - self.hparams.scores_alpha) * self.scores[eval_uid] + # Update moving average scores + self.moving_avg_scores[eval_uid] = self.ma_alpha * self.moving_avg_scores[eval_uid] + (1 - self.ma_alpha) * score + # Compute weights from moving average scores + weights = torch.softmax(self.moving_avg_scores, dim=0) + + # Log per-UID metrics + valid_score_indices = torch.nonzero(self.scores > 0).squeeze().view(-1) + for uid_i in valid_score_indices: + uid = uid_i.item() + self.wandb.log({ + f"validator/scores/{uid}": self.scores[uid_i].item(), + f"validator/moving_avg_scores/{uid}": self.moving_avg_scores[uid_i].item(), + f"validator/weights/{uid}": weights[uid_i].item(), + f"validator/stakes/{uid}": self.metagraph.S[uid_i].item(), + f"validator/current_score/{uid}": score if uid == eval_uid else 0, }, step=self.global_step) - for hotkey, slice_metric in window_metric.items(): - uid = self.metagraph.hotkeys.index(hotkey) - wandb.log({ - f"miner/loss/{uid}": slice_metric['loss'], - f"miner/tokens_per_step/{uid}": slice_metric['tokens_per_step'], - f"miner/tokens_per_second/{uid}": slice_metric['tokens_per_second'], - f"miner/sample_rate/{uid}": slice_metric['sample_rate'], - f"miner/learning_rate/{uid}": slice_metric['learning_rate'], - }, step=self.global_step) - - for uid_i in valid_score_indices: - wandb.log({ - f"validator/step_scores/{uid_i.item()}": self.step_scores[uid_i].item(), - f"validator/moving_scores/{uid_i.item()}": self.scores[uid_i].item(), - f"validator/weights/{uid_i.item()}": self.weights[uid_i].item(), - }, step=self.global_step) - # Set temperatured weights on the chain. - if self.global_step % 100 == 0: - # Check if all scores are zero - if torch.all(self.weights[self.metagraph.uids] == 0): - tplr.logger.info("All weights are zero, skipping weight setting") - continue - - tplr.logger.info(f"Setting weights on chain: {self.weights[self.metagraph.uids]}") - - max_retries = 3 - retry_delay = 5 - - for attempt in range(max_retries): - result, error = await self.set_weights_with_timeout() - - if result is not None: - tplr.logger.info(f"Successfully set weights on chain: {result}") - break - - if attempt < max_retries - 1: - tplr.logger.warning(f"Failed to set weights (attempt {attempt + 1}/{max_retries}): {error}") - tplr.logger.info(f"Retrying in {retry_delay} seconds...") - await asyncio.sleep(retry_delay) - else: - tplr.logger.error(f"Failed to set weights after {max_retries} attempts: {error}") - # Continue with the next iteration rather than freezing - break - - # Add periodic health check - self.last_active_timestamp = time.time() - - # Add this at the end of each main loop iteration - if time.time() - self.last_active_timestamp > 300: # 5 minutes timeout - tplr.logger.error("Validator appears to be frozen. Initiating recovery...") - # Force proceed to next iteration - continue - - except KeyboardInterrupt: - tplr.logger.info("Training interrupted by user. Stopping the run.") - self.stop_event.set() - await self.update_task - break # Exit the loop to reach the finally block - - except Exception as e: - tplr.logger.exception(f"Exception during training loop: {e}") - continue + # Log aggregate network statistics + self.wandb.log({ + "validator/active_miners": len(valid_score_indices), + "validator/mean_score": self.scores[valid_score_indices].mean().item(), + "validator/mean_moving_avg_score": self.moving_avg_scores[valid_score_indices].mean().item(), + "validator/max_score": self.scores.max().item(), + "validator/min_score": self.scores.min().item(), + "validator/max_moving_avg_score": self.moving_avg_scores.max().item(), + "validator/min_moving_avg_score": self.moving_avg_scores.min().item(), + "validator/mean_weight": weights[valid_score_indices].mean().item(), + "validator/weight_std": weights[valid_score_indices].std().item(), + "validator/score_std": self.scores[valid_score_indices].std().item(), + "validator/moving_avg_score_std": self.moving_avg_scores[valid_score_indices].std().item(), + }, step=self.global_step) + + # Set weights on chain + self.subtensor.set_weights( + wallet=self.wallet, + netuid=self.config.netuid, + uids=self.metagraph.uids, + weights=weights, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + tplr.logger.info(f'Set weights on chain for window {self.sync_window}') + + # Log weight update metrics + self.wandb.log({ + "validator/weight_update_window": self.sync_window, + "validator/mean_weight": weights.mean().item(), + "validator/max_weight": weights.max().item(), + "validator/min_weight": weights.min().item(), + "validator/weight_std": weights.std().item(), + }, step=self.global_step) + + # Apply the optimizer step + tplr.logger.info("Finish and step.") + self.optimizer.step() + self.scheduler.step() + tplr.logger.info(f"Total optimization steps: {self.global_step}") - finally: - # Wait for any pending checkpoint tasks to complete - if self.checkpoint_tasks: - tplr.logger.info(f"Waiting for {len(self.checkpoint_tasks)} checkpoint tasks to complete...") - await asyncio.gather(*self.checkpoint_tasks) - self.checkpoint_manager.cleanup() - tplr.logger.info("Validator shutdown complete.") - - # Returns the slice window based on a block. - def block_to_window(self, block: int) -> int: - return int(block / self.hparams.window_length) - # Returns the slice window based on a blotplr. - def window_to_seed(self, window: int) -> int: - return str( self.subtensor.get_block_hash( window * self.hparams.window_length ) ) - - # A listener thread which posts the block event - # when the chain announces a new block. def block_listener(self, loop): def handler(event, _u, _s): self.current_block = int(event['header']['number']) - loop.call_soon_threadsafe(self.block_event.set) - if self.block_to_window(self.current_block) != self.current_window: - self.window_seeds[ self.block_to_window(self.current_block) ] = self.window_to_seed( self.block_to_window(self.current_block) ) - self.current_window = self.block_to_window(self.current_block) - self.window_duration = tplr.T() - self.window_time if hasattr(self, 'window_time') else 0 - self.window_time = tplr.T() - loop.call_soon_threadsafe(self.new_window_event.set) - tplr.logger.info(f"{tplr.P(self.current_window, self.window_duration)} New Window.") - - # Run listener with retry. + if int(self.current_block / self.hparams.blocks_per_window) != self.current_window: + self.current_window = int(self.current_block / self.hparams.blocks_per_window) while not self.stop_event.is_set(): try: bt.subtensor(config=self.config).substrate.subscribe_block_headers(handler) break - except Exception as e: - tplr.logger.error(f"Failed to subscribe to block headers: {e}.\nRetrying in 1 seconds...") + except Exception: time.sleep(1) - async def set_weights_with_timeout(self, timeout=30): - """Set weights with timeout and retry logic""" - try: - # Wrap synchronous subtensor call in partial to pass arguments - set_weights_fn = partial( - self.subtensor.set_weights, - wallet=self.wallet, - netuid=self.config.netuid, - uids=self.metagraph.uids, - weights=self.weights[self.metagraph.uids], - version_key= tplr.version_key, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - - # Execute with timeout - result = await asyncio.wait_for( - asyncio.to_thread(set_weights_fn), - timeout=timeout - ) - return result, None - except TimeoutError: - return None, "Timeout while setting weights" - except Exception as e: - return None, str(e) - - async def save_checkpoint_background(self, global_step: int, block_number: int, scores: torch.Tensor, weights: torch.Tensor): - """Handles checkpoint saving and uploading in the background""" - try: - async with self.checkpoint_lock: # Ensure thread safety - await self.checkpoint_manager.save_and_upload( - global_step=global_step, - block_number=block_number, - scores=scores, - weights=weights, - optimizer_state=self.optimizer.state_dict(), - scheduler_state=self.scheduler.state_dict() - ) - except Exception as e: - tplr.logger.error(f"Error in background checkpoint save: {e}") - - def cleanup(self): - """Cleanup resources if needed.""" - self._shutdown = True - # Wait for any pending checkpoint tasks to complete - if self.checkpoint_tasks: - tplr.logger.info(f"Waiting for {len(self.checkpoint_tasks)} checkpoint tasks to complete...") - asyncio.gather(*self.checkpoint_tasks) - self.checkpoint_manager.cleanup() - tplr.logger.info("CheckpointManager shutdown complete") - if __name__ == "__main__": - validator = Validator() - asyncio.run(validator.run()) + asyncio.run(Validator().run()) diff --git a/pyproject.toml b/pyproject.toml index 67b53e2..5f2ec25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,47 +3,22 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "templar" +name = "tplr" version = "0.1.0" -description = "Incentivized Foundation Model Training" +description = "Add your description here" readme = "README.md" -license = { text = "MIT" } requires-python = ">=3.11" -authors = [ - { name = "const", email = "dont.email.me@gmail.com" } -] - dependencies = [ - "aiohttp==3.10.11", "bittensor==8.5.1", - "substrate-interface", - "boto3==1.34.131", - "safetensors==0.4.5", - "torch>=2.4.0", - "transformers==4.44.2", - "python-dotenv==1.0.1", - "datasets==3.0.0", - "torchvision==0.19.1", - "wandb>=0.18.3", - "typer==0.12.5", - "numpy==2.0.1", - "aioboto3>=13.1.1", - "loguru==0.7.2", - "uvloop==0.20.0", - "aiofiles>=24.1.0", - "pydantic>=2.9.2", -] - -[tool.uv] -dev-dependencies = [ - "pytest>=8.3.3", - "nest_asyncio>=1.5.8", - "pytest-asyncio" + "bt-decode==0.4.0", + "torch", + "boto3", + "einops", + "aiofiles", + "aioboto3", + "aiobotocore", + "transformers", + "pip", + "wandb", + "python-dotenv", ] - -[project.urls] -Homepage = "https://github.com/tplr-ai/templar" - -[tool.setuptools.packages.find] -where = ["src"] - diff --git a/scripts/clean.py b/scripts/clean.py deleted file mode 100644 index 745acea..0000000 --- a/scripts/clean.py +++ /dev/null @@ -1,69 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2024 Chakana.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os -import boto3 -import typer -from dotenv import dotenv_values -from templar.constants import CF_REGION_NAME - -env_config = {**dotenv_values(".env"), **os.environ} -AWS_ACCESS_KEY_ID = env_config.get("AWS_ACCESS_KEY_ID") -AWS_SECRET_ACCESS_KEY = env_config.get("AWS_SECRET_ACCESS_KEY") -CLIENT: boto3.client = boto3.client( - "s3", - region_name=CF_REGION_NAME, - aws_access_key_id=AWS_ACCESS_KEY_ID, - aws_secret_access_key=AWS_SECRET_ACCESS_KEY, -) - - -def main( - bucket: str = "decis", -): - # Create your S3 connection. - client: boto3.client = boto3.client( - "s3", - region_name=CF_REGION_NAME, - aws_access_key_id=AWS_ACCESS_KEY_ID, - aws_secret_access_key=AWS_SECRET_ACCESS_KEY, - ) - continuation_token = None - while True: - if continuation_token: - response = client.list_objects_v2( - Bucket=bucket, ContinuationToken=continuation_token - ) - else: - response = client.list_objects_v2(Bucket=bucket) - - file_names = [content["Key"] for content in response.get("Contents", [])] - - # Delete all the filenames - for file_name in file_names: - client.delete_object(Bucket=bucket, Key=file_name) - print(f"Deleted {file_name}") - - # Check if there are more files to delete - continuation_token = response.get("NextContinuationToken") - if not continuation_token: - break - - -# Main function. -if __name__ == "__main__": - typer.run(main) diff --git a/scripts/clean_testnet.py b/scripts/clean_testnet.py deleted file mode 100644 index 8391e37..0000000 --- a/scripts/clean_testnet.py +++ /dev/null @@ -1,87 +0,0 @@ -import yaml -from pathlib import Path -import boto3 -import shutil - -import templar as tplr - - -def clean_r2_bucket(): - """Clean all checkpoint files from R2 bucket""" - try: - # Load credentials from .env.yaml - env_path = Path(__file__).parent.parent / ".env.yaml" - with open(env_path, "r") as f: - config = yaml.safe_load(f) - - # Get R2 credentials - account_id = config.get("account_id") - write_creds = config.get("write", {}) - access_key_id = write_creds.get("access_key_id") - secret_access_key = write_creds.get("secret_access_key") - - if not all([account_id, access_key_id, secret_access_key]): - raise ValueError("Missing required R2 credentials in .env.yaml") - - # R2 connection settings - session = boto3.Session( - aws_access_key_id=access_key_id, aws_secret_access_key=secret_access_key - ) - - s3 = session.client( - service_name="s3", - endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com", - ) - - # List and delete all objects - paginator = s3.get_paginator("list_objects_v2") - - deleted_count = 0 - for page in paginator.paginate(Bucket=account_id): - if "Contents" in page: - for obj in page["Contents"]: - s3.delete_object(Bucket=account_id, Key=obj["Key"]) - deleted_count += 1 - - tplr.logger.success(f"Deleted {deleted_count} files from R2 bucket") - - except FileNotFoundError: - tplr.logger.error("Could not find .env.yaml file") - except yaml.YAMLError: - tplr.logger.error("Error parsing .env.yaml file") - except Exception as e: - tplr.logger.error(f"Error cleaning R2 bucket: {str(e)}") - - -def clean_local_folders(): - """Clean local wandb and checkpoints folders""" - try: - # Get project root directory - root_dir = Path(__file__).parent.parent - - # Clean wandb - wandb_dir = root_dir / "wandb" - if wandb_dir.exists(): - shutil.rmtree(wandb_dir) - tplr.logger.success("Cleaned wandb folder") - - # Clean checkpoints - checkpoints_dir = root_dir / "checkpoints" - if checkpoints_dir.exists(): - shutil.rmtree(checkpoints_dir) - tplr.logger.success("Cleaned checkpoints folder") - - except Exception as e: - tplr.logger.error(f"Error cleaning local folders: {str(e)}") - - -def main(): - """Main function to clean both R2 bucket and local folders""" - tplr.logger.info("Starting cleanup process...") - clean_r2_bucket() - clean_local_folders() - tplr.logger.success("Cleanup completed!") - - -if __name__ == "__main__": - main() diff --git a/scripts/decay.py b/scripts/decay.py deleted file mode 100644 index aa69776..0000000 --- a/scripts/decay.py +++ /dev/null @@ -1,345 +0,0 @@ -import os -import shutil -import sys -import json -import torch -import random -import asyncio -import numpy as np -from typing import Optional -from transformers import LlamaForCausalLM -import bittensor as bt -import argparse -import torch.optim as optim - -import templar as tplr - - -class DecayAgent: - @staticmethod - def config(): - parser = argparse.ArgumentParser(description="Decay agent script") - parser.add_argument( - "--project", type=str, default="templar", help="Optional wandb project name" - ) - parser.add_argument( - "--netuid", type=int, default=3, help="Bittensor network UID." - ) - parser.add_argument( - "--actual_batch_size", - type=int, - default=8, - help="Training batch size per accumulation.", - ) - parser.add_argument( - "--device", type=str, default="cuda", help="Device to use for training" - ) - parser.add_argument("--debug", action="store_true", help="Enable debug logging") - parser.add_argument("--trace", action="store_true", help="Enable trace logging") - parser.add_argument("--test", action="store_true", help="Run on test network") - parser.add_argument("--local", action="store_true", help="Run on local network") - parser.add_argument( - "--checkpoint_path", - type=str, - default=None, - help="Path to save/load the checkpoint", - ) - parser.add_argument( - "--save-location", - type=str, - default=None, - help="Directory to save/load slice files", - ) - bt.wallet.add_args(parser) - bt.subtensor.add_args(parser) - config = bt.config(parser) - if config.test: - config.subtensor.network = "test" - config.subtensor.chain_endpoint = "wss://test.finney.opentensor.ai:443/" - elif config.local: - config.subtensor.network = "local" - config.subtensor.chain_endpoint = "ws://127.0.0.1:9944" - if config.debug: - tplr.debug() - if config.trace: - tplr.trace() - return config - - def __init__(self): - # Init config - self.config = DecayAgent.config() - tplr.logger.info("\n" + "-" * 40 + " Config " + "-" * 40) - tplr.logger.info(self.config) - - # Init bittensor objects - self.wallet = bt.wallet(config=self.config) - self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(netuid=self.config.netuid) - self.chain_manager = tplr.chain.ChainManager( - subtensor=self.subtensor, wallet=self.wallet, netuid=self.config.netuid - ) - if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: - tplr.logger.error( - f"\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]. You need to register first with: [blue]`btcli subnet register`[/blue]\n" - ) - sys.exit() - self.uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) - tplr.logger.info("\n" + "-" * 40 + " Objects " + "-" * 40) - tplr.logger.info( - f"\nWallet: {self.wallet}\nSubtensor: {self.subtensor}\nMetagraph: {self.metagraph}\nUID: {self.uid}" - ) - - # Set up paths - self.checkpoint_path = os.path.join( - "checkpoints", - "decay", - f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}", - ) - os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) - - # Initialize wandb - wandb_dir = os.path.join(os.getcwd(), "wandb") - os.makedirs(wandb_dir, exist_ok=True) - run_id_file = os.path.join( - wandb_dir, f"wandb_run_id_D{self.uid}_{tplr.__version__}.txt" - ) - - if os.path.exists(run_id_file): - with open(run_id_file, "r") as f: - run_id = f.read().strip() - tplr.logger.info(f"Resuming WandB run with id {run_id}") - else: - run_id = None - tplr.logger.info("Starting new WandB run") - - self.wandb = tplr.initialize_wandb( - run_prefix="D", - uid=self.uid, - config=self.config, - group="decay", - job_type="decay_training", - ) - - # Load model and configuration - self.hparams = tplr.load_hparams() - torch.manual_seed(42) - np.random.seed(42) - random.seed(42) - - self.model = LlamaForCausalLM(config=self.hparams.model_config) - self.model.to(self.config.device) - - # Initialize optimizer with decay schedule - self.optimizer = optim.AdamW( - self.model.parameters(), - lr=self.hparams.learning_rate, - betas=(self.hparams.optimizer_beta1, self.hparams.optimizer_beta2), - weight_decay=self.hparams.optimizer_weight_decay, - foreach=True, - ) - - # Initialize decay scheduler - self.scheduler = tplr.get_wsd_scheduler( - optimizer=self.optimizer, - num_warmup_steps=0, # No warmup for decay - num_stable_steps=0, # No stable phase - num_decay_steps=self.hparams.num_decay_steps, - ) - - # Initialize checkpoint manager - self.checkpoint_manager = tplr.CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device=self.config.device, - optimizer=self.optimizer, - scheduler=self.scheduler, - ) - - # Load initial checkpoint with optimizer state - self.global_step = asyncio.run( - self.checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets, - optimizer=self.optimizer, - scheduler=self.scheduler, - is_validator=False, - hparams=self.hparams, - ) - ) - - # Get buckets for all neurons - self.buckets = tplr.get_all_buckets( - subtensor=self.subtensor, - netuid=self.config.netuid, - metagraph=self.metagraph, - ) - - # Initialize state - self.last_eval_step = 0 - self.last_block_number = 0 - self.global_step = 0 - - async def decay_and_evaluate(self) -> Optional[int]: - """Performs decay training and evaluation""" - try: - if self.global_step == 0: - tplr.logger.error("Failed to load checkpoint from highest stake neuron") - return None - - # Start decay training - tplr.logger.info(f"Starting decay training from step {self.global_step}") - - for step in range(self.hparams.num_decay_steps): - # Download training data - pages = await tplr.dataset.DatasetLoader.next_pages( - offset=self.block_number, n_pages=1, seed=self.uid - ) - - dataset = await tplr.dataset.DatasetLoader.create( - batch_size=self.config.actual_batch_size, - sequence_length=self.hparams.sequence_length, - pages_info=pages, - tokenizer=self.hparams.tokenizer, - ) - - # Training step - self.model.train() - total_loss = 0.0 - - for batch in dataset: - input_ids = torch.tensor(batch, dtype=torch.long).to( - self.model.device - ) - labels = input_ids.clone() - labels = torch.where( - labels == self.hparams.tokenizer.pad_token_id, -100, labels - ) - - with torch.amp.autocast( - device_type=self.model.device.type, dtype=torch.bfloat16 - ): - outputs = self.model(input_ids=input_ids, labels=labels) - - total_loss += outputs.loss.item() - outputs.loss.backward() - - if self.hparams.grad_clip: - torch.nn.utils.clip_grad_norm_( - self.model.parameters(), self.hparams.grad_clip - ) - - self.optimizer.step() - self.scheduler.step() - self.optimizer.zero_grad() - - current_lr = self.scheduler.get_last_lr()[0] - step_loss = total_loss / len(dataset) - - # Log training metrics - self.wandb.log( - { - "decay/loss": step_loss, - "decay/learning_rate": current_lr, - "decay/progress": step / self.hparams.num_decay_steps, - "global_step": self.global_step + step, - } - ) - - # Run evaluation periodically - if (step + 1) % self.config.eval_interval == 0: - await self.evaluate(self.global_step + step) - - tplr.logger.info( - f"Decay step {step}/{self.hparams.num_decay_steps}, Loss: {step_loss:.4f}, LR: {current_lr:.2e}" - ) - - tplr.logger.info("Decay training completed") - return self.global_step + self.hparams.num_decay_steps - - except Exception as e: - tplr.logger.error(f"Error during decay training: {str(e)}") - return None - - async def evaluate(self, global_step: int) -> None: - """Runs evaluation on the current model state""" - try: - # Save model and tokenizer for evaluation - model_path = "models/eval" - os.makedirs(model_path, exist_ok=True) - self.model.save_pretrained(model_path) - self.hparams.tokenizer.save_pretrained(model_path) - - # Create results directory - results_dir = f"{model_path}/results" - os.makedirs(results_dir, exist_ok=True) - - # Run evaluation - lm_eval_command = ( - f"lm-eval " - f"--model hf " - f"--model_args pretrained=models/eval,tokenizer=models/eval " - f"--tasks arc_challenge,arc_easy,hellaswag,openbookqa,piqa,winogrande " - f"--device {self.config.device} " - f"--batch_size 6 " - f"--output_path {results_dir}" - ) - - process = await asyncio.create_subprocess_shell( - lm_eval_command, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - stdout, stderr = await process.communicate() - - if process.returncode != 0: - tplr.logger.error(f"Evaluation failed: {stderr.decode()}") - return - - # Process and log results - eval_results_dir = os.path.join(results_dir, "models__eval") - if not os.path.exists(eval_results_dir): - tplr.logger.error(f"Results directory not found: {eval_results_dir}") - return - - latest_file = max( - [ - os.path.join(eval_results_dir, f) - for f in os.listdir(eval_results_dir) - ], - key=os.path.getctime, - ) - - with open(latest_file, "r") as f: - results = json.load(f) - - # Log results to wandb - for task_name, task_results in results["results"].items(): - metric_name = ( - "acc_norm,none" if task_name != "winogrande" else "acc,none" - ) - if metric_value := task_results.get(metric_name): - tplr.logger.info(f"{task_name}: {metric_value}") - self.wandb.log( - { - f"eval/{task_name}": metric_value, - }, - step=global_step, - ) - - # Cleanup - shutil.rmtree(model_path) - torch.cuda.empty_cache() - - except Exception as e: - tplr.logger.error(f"Error during evaluation: {str(e)}") - - -async def main(): - decay_agent = DecayAgent() - await decay_agent.decay_and_evaluate() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/scripts/docker_run b/scripts/docker_run new file mode 100644 index 0000000..6de9a6a --- /dev/null +++ b/scripts/docker_run @@ -0,0 +1,55 @@ +#!/bin/bash +set -e + +# Function to prompt for input with default value +prompt_with_default() { + local prompt="$1" + local default="$2" + local response + + echo -n "$prompt [$default]: " + read response + echo "${response:-$default}" +} + +# Function to prompt for required input +prompt_required() { + local prompt="$1" + local response="" + + while [ -z "$response" ]; do + echo -n "$prompt: " + read response + if [ -z "$response" ]; then + echo "This field is required" + fi + done + echo "$response" +} + +# Prompt for configuration +echo "🤖 Templar Mining Docker Configuration" +echo "-------------------------------------" + +NODE_TYPE=$(prompt_with_default "Enter node type (miner/validator)" "miner") +WALLET_NAME=$(prompt_required "Enter wallet name") +WALLET_HOTKEY=$(prompt_required "Enter wallet hotkey") +WANDB_API_KEY=$(prompt_required "Enter Weights & Biases API key") +NETWORK=$(prompt_with_default "Enter network" "test") +CUDA_DEVICE=$(prompt_with_default "Enter CUDA device" "cuda:0") +DEBUG=$(prompt_with_default "Enable debug mode? (true/false)" "false") + +# Export variables for docker-compose +export WALLET_NAME WALLET_HOTKEY WANDB_API_KEY NETWORK CUDA_DEVICE DEBUG + +# Choose compose file based on node type +COMPOSE_FILE="docker/compose.${NODE_TYPE}.yml" + +# Start the containers +echo -e "\n📦 Starting containers..." +docker compose -f "$COMPOSE_FILE" up -d + +echo -e "\n✅ Containers started successfully!" +echo "📝 Logs are available in the ./logs directory" +echo -e "\nTo follow logs, run:" +echo "docker compose -f $COMPOSE_FILE logs -f" \ No newline at end of file diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..6d10d67 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -e + +# Check required environment variables +for var in WALLET_NAME WALLET_HOTKEY NODE_TYPE WANDB_API_KEY NETUID; do + if [ -z "${!var}" ]; then + echo "Error: $var environment variable is required" + exit 1 + fi +done + +# Activate virtual environment +source /app/.venv/bin/activate + +# Create logs directory +mkdir -p /app/logs + +# Check CUDA availability +if ! python3 -c "import torch; assert torch.cuda.is_available(), 'CUDA not available'"; then + echo "Error: CUDA is not available" + exit 1 +fi + +# Login to wandb non-interactively +wandb login ${WANDB_API_KEY} --relogin + +# Convert DEBUG to --debug flag if true +DEBUG_FLAG="" +if [ "$DEBUG" = "true" ]; then + DEBUG_FLAG="--debug" +fi + +# Check CUDA version +CUDA_VERSION=$(python3 -c "import torch; print(torch.version.cuda)") +if [[ "${CUDA_VERSION}" != "12.6" ]]; then + echo "Warning: Container CUDA version (${CUDA_VERSION}) differs from host CUDA version (12.6)" +fi + +# Check NODE_TYPE and start appropriate process +if [ "$NODE_TYPE" = "miner" ]; then + echo "Starting miner..." + exec python3 neurons/miner.py \ + --wallet.name ${WALLET_NAME} \ + --wallet.hotkey ${WALLET_HOTKEY} \ + --netuid ${NETUID} \ + --device ${CUDA_DEVICE} \ + --subtensor.network ${NETWORK} \ + --use_wandb \ + ${DEBUG_FLAG} +elif [ "$NODE_TYPE" = "validator" ]; then + echo "Starting validator..." + exec python3 neurons/validator.py \ + --wallet.name ${WALLET_NAME} \ + --wallet.hotkey ${WALLET_HOTKEY} \ + --netuid ${NETUID} \ + --device ${CUDA_DEVICE} \ + --subtensor.network ${NETWORK} \ + --use_wandb \ + ${DEBUG_FLAG} +else + echo "Error: NODE_TYPE must be either \"miner\" or \"validator\"" + exit 1 +fi \ No newline at end of file diff --git a/scripts/eval.py b/scripts/eval.py index b3cc9b0..93e405a 100644 --- a/scripts/eval.py +++ b/scripts/eval.py @@ -1,11 +1,26 @@ # The MIT License (MIT) # © 2024 templar.tech +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + import os import json -import asyncio import shutil import torch +import asyncio import argparse import bittensor as bt from transformers import LlamaForCausalLM diff --git a/scripts/release_notes.rs b/scripts/release_notes.rs deleted file mode 100755 index 73cbde4..0000000 --- a/scripts/release_notes.rs +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env rust-script -// ^ `cargo install rust-script` to be able to run this script - -use core::{fmt::Display, str::FromStr}; -use std::{env, process::Command}; - -fn eval(cmd: impl Display, print: bool) -> Result { - if print { - println!("$ {}", cmd); - } - let output = Command::new("sh") - .arg("-c") - .arg(cmd.to_string()) - .output() - .expect("failed to execute process"); - if print { - println!("{}", String::from_utf8(output.stdout.clone()).unwrap()); - eprintln!("{}", String::from_utf8(output.stderr.clone()).unwrap()); - } - if !output.status.success() { - return Err(String::from_utf8(output.stderr).unwrap()); - } - Ok(String::from_utf8(output.stdout).unwrap().trim().to_string()) -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -enum Network { - Mainnet, - Testnet, -} - -impl FromStr for Network { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "mainnet" => Ok(Network::Mainnet), - "testnet" => Ok(Network::Testnet), - _ => Err(()), - } - } -} - -fn main() { - let network = env::var("NETWORK") - .unwrap_or_else(|_| "mainnet".to_string()) - .parse::() - .unwrap_or_else(|_| panic!("Invalid NETWORK value")); - println!("Network: {:?}", network); - - let all_tags = env::var("PREVIOUS_TAG") - .unwrap_or_else(|_| eval("git tag --sort=-creatordate", false).unwrap()) - .split("\n") - .map(|s| s.trim().to_string()) - .collect::>(); - - let previous_tag = match network { - Network::Mainnet => all_tags - .iter() - .find(|tag| tag.starts_with("v") && !tag.ends_with("-pre-release")) - .expect("could not find a valid mainnet tag!"), - Network::Testnet => all_tags - .iter() - .find(|tag| tag.starts_with("v") && tag.ends_with("-pre-release")) - .expect("could not find a valid testnet tag!"), - }; - println!("Previous release tag: {}", previous_tag); - - let branch = env::var("BRANCH").unwrap_or( - match network { - Network::Mainnet => "testnet", - Network::Testnet => "devnet", - } - .to_string(), - ); - println!("Branch: {}", branch); - - println!( - "Generating release notes for all merges since {}...", - previous_tag, - ); - let merges = eval( - format!( - "git log --merges --pretty=format:'%s' {}..{}", - branch, previous_tag, - ), - false, - ) - .unwrap() - .split("\n") - .map(|s| s.trim().to_string()) - .filter(|s| { - !s.is_empty() - && s.starts_with("Merge pull request #") - && !s.ends_with("from opentensor/devnet-ready") - && !s.ends_with("from opentensor/testnet-ready") - && !s.ends_with("from opentensor/devnet") - && !s.ends_with("from opentensor/testnet") - }) - .collect::>(); - - println!(""); - println!("Filtered merges:\n{}", merges.join("\n")); - - println!(""); - let pr_numbers = merges - .iter() - .map(|s| s.split(" ").collect::>()[3].trim_start_matches("#")) - .collect::>(); - println!("PR numbers:\n{:?}", pr_numbers); - - println!(""); - println!("Fetching PR titles..."); - let pr_titles = pr_numbers - .iter() - .map(|pr_number| { - print!("#{}: ", pr_number); - let title = eval(format!("gh pr view {} --json title", pr_number), false) - .unwrap() - .trim() - .to_string(); - if !title.starts_with("{\"title\":\"") { - panic!("Malformed PR title: {}", title); - } - let title = title - .trim_start_matches("{\"title\":\"") - .trim_end_matches("\"}") - .trim() - .to_string(); - println!("{}", title); - title - }) - .collect::>(); - - println!(""); - println!("Fetching PR authors..."); - let pr_authors = pr_numbers - .iter() - .map(|pr_number| { - print!("#{}: ", pr_number); - let author = eval( - format!("gh pr view {} --json author | jq .author.login", pr_number), - false, - ) - .unwrap() - .trim() - .trim_start_matches("\"") - .trim_end_matches("\"") - .to_string(); - println!("{}", author); - author - }) - .collect::>(); - - println!(""); - println!("generated release notes:"); - let release_notes = "## What's Changed\n".to_string(); - let release_notes = release_notes - + &pr_numbers - .iter() - .zip(pr_titles.iter()) - .zip(pr_authors.iter()) - .map(|((pr_number, pr_title), pr_author)| { - format!("- {} in #{} by @{}\n", pr_title, pr_number, pr_author) - }) - .collect::(); - println!("{}", release_notes); - - println!(""); - println!("writing release notes to /tmp/release_notes.md"); - std::fs::write("/tmp/release_notes.md", release_notes).unwrap(); - println!("done!"); -} diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100755 index c916200..0000000 --- a/scripts/run.sh +++ /dev/null @@ -1,750 +0,0 @@ -#!/usr/bin/env bash - -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -set -euo pipefail - -# Initialize default values -DEBUG=false -PROJECT="templar" -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" -BUCKET="" -NETWORK="" -NEURON_TYPE="" - -# Function to display help message -display_help() { - cat << EOF -Usage: $0 [options] - -Options: - --debug Enable debug mode - --project Set the project name (default: templar) - --aws-access-key-id Set AWS Access Key ID - --aws-secret-access-key Set AWS Secret Access Key - --bucket Set the S3 bucket name - --network Set the network (options: finney, test, local) - --neuron Set the neuron type (options: miner, validator) - -h, --help Display this help message - -Description: - Installs and runs a τemplar neuron on your GPU. If the --network option is not provided, you will be prompted to select a network. -EOF -} - -# Parse command-line arguments -while [[ $# -gt 0 ]]; do - key="$1" - case $key in - --debug) - DEBUG=true - shift - ;; - --project) - PROJECT="$2" - shift 2 - ;; - --aws-access-key-id) - AWS_ACCESS_KEY_ID="$2" - shift 2 - ;; - --aws-secret-access-key) - AWS_SECRET_ACCESS_KEY="$2" - shift 2 - ;; - --bucket) - BUCKET="$2" - shift 2 - ;; - --network) - NETWORK="$2" - shift 2 - ;; - --neuron) - NEURON_TYPE="$2" - shift 2 - ;; - -h|--help|-help|--h) - display_help - exit 0 - ;; - *) - # Only error if not a network argument - if [[ "$1" != "--network" ]]; then - echo "Unknown option: $1" - display_help - exit 1 - fi - shift - ;; - esac -done - - - -# Set up colors and styles for terminal output -if [[ -t 1 ]]; then - tty_escape() { printf "\033[%sm" "$1"; } -else - tty_escape() { :; } -fi -tty_mkbold() { tty_escape "1;$1"; } -tty_blue="$(tty_mkbold 34)" -tty_red="$(tty_mkbold 31)" -tty_green="$(tty_mkbold 32)" -tty_yellow="$(tty_mkbold 33)" -tty_bold="$(tty_mkbold 39)" -tty_reset="$(tty_escape 0)" - -# Logging functions for standardized output -ohai() { - printf "${tty_blue}==>${tty_bold} %s${tty_reset}\n" "$*" -} - -pdone() { - printf " ${tty_green}[✔]${tty_bold} %s${tty_reset}\n" "$*" -} - -info() { - printf "${tty_green}%s${tty_reset}\n" "$*" -} - -warn() { - printf "${tty_yellow}Warning${tty_reset}: %s\n" "$*" >&2 -} - -error() { - printf "${tty_red}Error${tty_reset}: %s\n" "$*" >&2 -} - -abort() { - error "$@" - exit 1 -} - -trap 'abort "An unexpected error occurred."' ERR - -# Function to get a single character input from the user -getc() { - local save_state - save_state="$(/bin/stty -g)" - /bin/stty raw -echo - IFS='' read -r -n 1 -d '' "$@" - /bin/stty "${save_state}" -} - -# Function to pause execution and wait for user confirmation -wait_for_user() { - local c - echo - echo -e "${tty_bold}Press ${tty_green}RETURN/ENTER${tty_reset} ${tty_bold}to continue or any other key to abort:${tty_reset}" - getc c - if ! [[ "${c}" == $'\r' || "${c}" == $'\n' ]] - then - exit 1 - fi -} - -# Function to execute a command with logging -execute() { - ohai "Running: $*" - if ! "$@"; then - abort "Failed during: $*" - fi -} - -# Function to check for sudo access -have_sudo_access() { - if ! command -v sudo &> /dev/null; then - warn "sudo command not found. Please install sudo or run as root." - return 1 - fi - if [ "$EUID" -ne 0 ]; then - if ! sudo -n true 2>/dev/null; then - warn "This script requires sudo access to install packages. Please run as root or ensure your user has sudo privileges." - return 1 - fi - fi - return 0 -} - -# Function to execute commands with sudo if necessary -execute_sudo() { - if have_sudo_access; then - ohai "sudo $*" - if ! sudo "$@"; then - abort "Failed to execute: sudo $*" - fi - else - warn "Sudo access is required, attempting to run without sudo" - ohai "$*" - if ! "$@"; then - abort "Failed to execute: $*" - fi - fi -} - -# Function to set or replace environment variables in bash_profile -set_or_replace_env_var() { - local var_name="$1" - local var_value="$2" - local profile_file="$3" - - # Escape special characters for sed - local escaped_var_value=$(printf '%s\n' "$var_value" | sed -e 's/[\/&]/\\&/g') - - if grep -q "^export $var_name=" "$profile_file"; then - # Variable exists, replace it - sed -i.bak "s/^export $var_name=.*/export $var_name=\"$escaped_var_value\"/" "$profile_file" - else - # Variable does not exist, append it - echo "export $var_name=\"$var_value\"" >> "$profile_file" - fi -} - -# Clear screen -clear - -# Display the logo -printf '%s\n' "___ _ _ _ _ | _ _ " -printf '%s\n' " | (/_| | ||_)|(_|| " -printf '%s\n' " | | " -echo "" - -echo -e "\nWelcome to the τemplar Installation Script\n" - -echo -e "This script will:\n" -echo -e "1. Install required software (Git, npm, pm2, rust, uv, Python 3.12)" -echo -e "2. Set up AWS credentials" -echo -e "3. Clone and set up the τemplar repository" -echo -e "4. Create and register Bittensor wallets" -echo -e "5. Configure wandb for logging" -echo -e "6. Clean the specified S3 bucket" -echo -e "7. Start τemplar neurons on available GPUs on your chosen network\n" - -echo -e "⚠️ Please ensure you have:" -echo -e " ✓ A stable internet connection" -echo -e " ✓ Sufficient permissions to install software\n" - - - -wait_for_user - -# If network not provided, prompt user to select one -if [[ -z "$NETWORK" ]]; then - echo "Please select a network:" - echo "1) finney" - echo "2) testnet" - echo "3) local" - read -p "Enter selection [1-3]: " network_choice - - case $network_choice in - 1) NETWORK="finney" ;; - 2) NETWORK="testnet" ;; - 3) NETWORK="local" ;; - *) - echo "Invalid selection" - exit 1 - ;; - esac -fi - -# Set network-specific variables based on the selected network -case "$NETWORK" in - finney|FINNEY|Finney) - SUBTENSOR_NETWORK="finney" - NETUID=3 - SUBTENSOR_CHAIN_ENDPOINT="" - PM2_NETWORK_OPTIONS="" - ;; - test|testnet|TEST|TESTNET|Testnet) - SUBTENSOR_NETWORK="test" - NETUID=223 - SUBTENSOR_CHAIN_ENDPOINT="wss://test.finney.opentensor.ai:443/" - PM2_NETWORK_OPTIONS="--test" - ;; - local|LOCAL|Local) - SUBTENSOR_NETWORK="local" - NETUID=3 - SUBTENSOR_CHAIN_ENDPOINT="wss://localhost:9944" - PM2_NETWORK_OPTIONS="" - ;; - *) - echo "Unknown network: $NETWORK" - display_help - exit 1 - ;; -esac - - -if [[ -z "$NEURON_TYPE" ]]; then - echo "Please select a neuron type:" - echo "1) miner" - echo "2) validator" - read -p "Enter selection [1-2]: " neuron_choice - - case $neuron_choice in - 1) NEURON_TYPE="miner" ;; - 2) NEURON_TYPE="validator" ;; - *) - echo "Invalid selection" - exit 1 - ;; - esac -fi - -# Validate neuron type -case "$NEURON_TYPE" in - miner|validator) - ;; - *) - echo "Invalid neuron type: $NEURON_TYPE" - display_help - exit 1 - ;; -esac - -# Ensure ~/.bash_profile exists -touch ~/.bash_profile -source ~/.bash_profile - -# Backup the bash_profile -cp ~/.bash_profile ~/.bash_profile.bak - -# Prompt the user for AWS credentials if not supplied via command-line -ohai "Getting AWS credentials ..." -if [[ -z "$AWS_ACCESS_KEY_ID" ]] || [[ -z "$AWS_SECRET_ACCESS_KEY" ]] || [[ -z "$BUCKET" ]]; then - warn "This script will store your AWS credentials in your ~/.bash_profile file." - warn "This is not secure and is not recommended." - read -p "Do you want to proceed? [y/N]: " proceed - if [[ "$proceed" != "y" && "$proceed" != "Y" ]]; then - abort "Aborted by user." - fi - - if [[ -z "$AWS_ACCESS_KEY_ID" ]]; then - read -p "Enter your AWS Access Key ID: " AWS_ACCESS_KEY_ID - fi - if [[ -z "$AWS_SECRET_ACCESS_KEY" ]]; then - read -p "Enter your AWS Secret Access Key: " AWS_SECRET_ACCESS_KEY - fi - if [[ -z "$BUCKET" ]]; then - read -p "Enter your S3 Bucket Name: " BUCKET - fi -fi - -# Overwrite or add the AWS credentials in the bash_profile -set_or_replace_env_var "AWS_ACCESS_KEY_ID" "$AWS_ACCESS_KEY_ID" ~/.bash_profile -set_or_replace_env_var "AWS_SECRET_ACCESS_KEY" "$AWS_SECRET_ACCESS_KEY" ~/.bash_profile -set_or_replace_env_var "BUCKET" "$BUCKET" ~/.bash_profile - -# Source the bash_profile to apply the changes -source ~/.bash_profile -pdone "AWS credentials set in ~/.bash_profile" - -ohai "Installing requirements ..." -# Install Git if not present -if ! command -v git &> /dev/null; then - ohai "Git not found. Installing git ..." - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - ohai "Detected Linux" - if [ -f /etc/os-release ]; then - . /etc/os-release - if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* ]]; then - ohai "Detected Ubuntu, installing Git..." - if [[ "$DEBUG" == "true" ]]; then - execute_sudo apt-get update -y - execute_sudo apt-get install git -y - else - execute_sudo apt-get update -y > /dev/null 2>&1 - execute_sudo apt-get install git -y > /dev/null 2>&1 - fi - else - warn "Unsupported Linux distribution: $ID" - abort "Cannot install Git automatically" - fi - else - warn "Cannot detect Linux distribution" - abort "Cannot install Git automatically" - fi - else - abort "Unsupported OS type: $OSTYPE" - fi -else - pdone "Git is already installed" -fi - -# Check for Rust installation -if ! command -v rustc &> /dev/null; then - ohai "Installing Rust ..." - if [[ "$DEBUG" == "true" ]]; then - execute curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - else - execute curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y > /dev/null 2>&1 - fi - # Add Rust to the PATH for the current session - source $HOME/.cargo/env -fi -pdone "Rust is installed" - -# Install uv if not present -if ! command -v uv &> /dev/null; then - ohai "Installing uv ..." - if [[ "$DEBUG" == "true" ]]; then - execute curl -LsSf https://astral.sh/uv/install.sh | sh - else - execute curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1 - fi - # Add uv to the PATH for the current session - export PATH="$HOME/.cargo/bin:$PATH" -fi -pdone "uv is installed" - -# Check if npm is installed -if ! command -v npm &> /dev/null; then - ohai "Installing npm ..." - if ! command -v node &> /dev/null; then - ohai "Node.js could not be found, installing..." - if ! curl -fsSL https://deb.nodesource.com/setup_18.x | bash; then - abort "Failed to download Node.js setup script" - fi - if ! execute_sudo apt-get install -y nodejs; then - abort "Failed to install Node.js" - fi - fi - if ! curl -L https://www.npmjs.com/install.sh | sh; then - abort "Failed to install npm" - fi -fi -pdone "npm is installed" - -# Install pm2 -if ! command -v pm2 &> /dev/null; then - ohai "Installing pm2 ..." - if [[ "$DEBUG" == "true" ]]; then - execute npm install pm2 -g - else - execute npm install pm2 -g > /dev/null 2>&1 - fi -fi -pdone "pm2 is installed" - -ohai "Installing τemplar ..." -# Check if we are inside the τemplar repository -if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then - REPO_PATH="." -else - if [ ! -d "τemplar" ]; then - ohai "Cloning τemplar ..." - execute git clone https://github.com/RaoFoundation/templar - REPO_PATH="templar" - else - REPO_PATH="templar" - fi -fi -pdone "τemplar repository is ready at $REPO_PATH" - -# Install Python 3.12 if not installed -if ! command -v python3.12 &> /dev/null; then - ohai "Installing python3.12 ..." - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - ohai "Detected Linux" - if [ -f /etc/os-release ]; then - . /etc/os-release - if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* ]]; then - ohai "Detected Ubuntu, installing Python 3.12..." - if [[ "$DEBUG" == "true" ]]; then - if have_sudo_access; then - execute_sudo add-apt-repository ppa:deadsnakes/ppa -y - else - warn "Skipping add-apt-repository due to lack of sudo access" - fi - execute_sudo apt-get update -y - else - if have_sudo_access; then - execute_sudo add-apt-repository ppa:deadsnakes/ppa -y > /dev/null 2>&1 - else - warn "Skipping add-apt-repository due to lack of sudo access" - fi - execute_sudo apt-get update -y > /dev/null 2>&1 - execute_sudo apt-get install --reinstall python3-apt > /dev/null 2>&1 - execute_sudo apt-get install python3.12 -y > /dev/null 2>&1 - execute_sudo apt-get install python3.12-venv > /dev/null 2>&1 - fi - else - warn "Unsupported Linux distribution: $ID" - abort "Cannot install Python 3.12 automatically" - fi - else - warn "Cannot detect Linux distribution" - abort "Cannot install Python 3.12 automatically" - fi - else - abort "Unsupported OS type: $OSTYPE" - fi -fi -pdone "Python 3.12 is installed" - -# Create a virtual environment if it does not exist -if [ ! -d "$REPO_PATH/venv" ]; then - ohai "Creating virtual environment at $REPO_PATH..." - if [[ "$DEBUG" == "true" ]]; then - execute uv venv "$REPO_PATH/.venv" - else - execute uv venv "$REPO_PATH/.venv" > /dev/null 2>&1 - fi -fi -pdone "Virtual environment is set up at $REPO_PATH" - - -# Activate the virtual environment -ohai "Activating virtual environment ..." -source $REPO_PATH/.venv/bin/activate -pdone "Virtual environment activated" - -ohai "Installing Python requirements ..." -cd "$REPO_PATH" - -# First, ensure uv is properly set up -if [[ "$DEBUG" == "true" ]]; then - execute uv pip install --upgrade pip -else - execute uv pip install --upgrade pip > /dev/null 2>&1 -fi - -# Install PyTorch first -if [[ "$DEBUG" == "true" ]]; then - execute uv pip install torch --index-url https://download.pytorch.org/whl/cu118 -else - execute uv pip install torch --index-url https://download.pytorch.org/whl/cu118 > /dev/null 2>&1 -fi - -# Now run uv sync -if [[ "$DEBUG" == "true" ]]; then - # remove prerelease once bt decode is released - execute uv sync --extra all --prerelease=allow -else - execute uv sync --extra all --prerelease=allow > /dev/null 2>&1 -fi - -# Install flash-attn separately due to its special requirements -if [[ "$DEBUG" == "true" ]]; then - execute uv pip install flash-attn --no-build-isolation -else - execute uv pip install flash-attn --no-build-isolation > /dev/null 2>&1 -fi - -pdone "Python requirements installed" - -# Check for GPUs -ohai "Checking for GPUs..." -if ! command -v nvidia-smi &> /dev/null; then - warn "nvidia-smi command not found. Please ensure NVIDIA drivers are installed." - NUM_GPUS=0 -else - NUM_GPUS=$(nvidia-smi --query-gpu=name --format=csv,noheader | wc -l) - - if [ "$NUM_GPUS" -gt 0 ]; then - nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | while read -r memory; do - pdone "Found GPU with $((memory / 1024)) GB of memory" - done - else - warn "No GPUs found on this machine." - fi -fi - -# Check system RAM -if command -v free &> /dev/null; then - TOTAL_RAM=$(free -g | awk '/^Mem:/{print $2}') - pdone "System RAM: ${TOTAL_RAM} GB" -else - warn "Cannot determine system RAM. 'free' command not found." -fi - -ohai "Creating wallets ..." - -# Create coldkey if it doesn't exist -exists_on_device=$(python3 -c "import bittensor as bt; w = bt.wallet(); print(w.coldkey_file.exists_on_device())" 2>/dev/null) -if [ "$exists_on_device" != "True" ]; then - if [[ "$DEBUG" == "true" ]]; then - echo "n" | btcli wallet new_coldkey --wallet.name default --n-words 12 - else - echo "n" | btcli wallet new_coldkey --wallet.name default --n-words 12 > /dev/null 2>&1 - fi -fi -pdone "Wallet 'default' is ready" - -# Create hotkeys based on neuron type -if [ "$NEURON_TYPE" = "validator" ]; then - # Create single hotkey for validator - HOTKEY_NAME="validator" - - # Check if hotkey exists - exists_on_device=$(python3 -c "import bittensor as bt; w = bt.wallet(hotkey='$HOTKEY_NAME'); print(w.hotkey_file.exists_on_device())" 2>/dev/null) - - if [ "$exists_on_device" != "True" ]; then - if [[ "$DEBUG" == "true" ]]; then - echo "n" | btcli wallet new_hotkey --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --n-words 12 - else - echo "n" | btcli wallet new_hotkey --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --n-words 12 > /dev/null 2>&1 - fi - pdone "Created Validator Hotkey '$HOTKEY_NAME'" - fi - - # Check registration status - is_registered=$(python3 -c "import bittensor as bt; w = bt.wallet(hotkey='$HOTKEY_NAME'); sub = bt.subtensor('$SUBTENSOR_NETWORK'); print(sub.is_hotkey_registered_on_subnet(hotkey_ss58=w.hotkey.ss58_address, netuid=$NETUID))") - - if [[ "$is_registered" != *"True"* ]]; then - ohai "Registering validator hotkey on netuid $NETUID" - if [[ "$DEBUG" == "true" ]]; then - btcli subnet pow_register --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --netuid "$NETUID" --subtensor.network "$SUBTENSOR_NETWORK" --no_prompt - else - btcli subnet pow_register --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --netuid "$NETUID" --subtensor.network "$SUBTENSOR_NETWORK" --no_prompt > /dev/null 2>&1 - fi - pdone "Registered Validator Hotkey on netuid $NETUID" - else - pdone "Validator Hotkey already registered on netuid $NETUID" - fi -else - # Create miner hotkeys - if [ "$NUM_GPUS" -gt 0 ]; then - for i in $(seq 0 $((NUM_GPUS - 1))); do - HOTKEY_NAME="C$i" - - # Check if hotkey exists - exists_on_device=$(python3 -c "import bittensor as bt; w = bt.wallet(hotkey='$HOTKEY_NAME'); print(w.hotkey_file.exists_on_device())" 2>/dev/null) - - if [ "$exists_on_device" != "True" ]; then - if [[ "$DEBUG" == "true" ]]; then - echo "n" | btcli wallet new_hotkey --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --n-words 12 - else - echo "n" | btcli wallet new_hotkey --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --n-words 12 > /dev/null 2>&1 - fi - pdone "Created Miner Hotkey '$HOTKEY_NAME'" - fi - - # Check registration status - is_registered=$(python3 -c "import bittensor as bt; w = bt.wallet(hotkey='$HOTKEY_NAME'); sub = bt.subtensor('$SUBTENSOR_NETWORK'); print(sub.is_hotkey_registered_on_subnet(hotkey_ss58=w.hotkey.ss58_address, netuid=$NETUID))") - - if [[ "$is_registered" != *"True"* ]]; then - ohai "Registering miner hotkey $HOTKEY_NAME on netuid $NETUID" - if [[ "$DEBUG" == "true" ]]; then - btcli subnet pow_register --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --netuid "$NETUID" --subtensor.network "$SUBTENSOR_NETWORK" --no_prompt - else - btcli subnet pow_register --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --netuid "$NETUID" --subtensor.network "$SUBTENSOR_NETWORK" --no_prompt > /dev/null 2>&1 - fi - pdone "Registered Miner Hotkey $HOTKEY_NAME on netuid $NETUID" - else - pdone "Miner Hotkey $HOTKEY_NAME already registered on netuid $NETUID" - fi - done - else - warn "No GPUs found. Exiting" - exit 1 - fi -fi -pdone "All hotkeys registered" - -# Initialize PM2 -ohai "Stopping old pm2 processes..." -if pm2 list | grep -q 'online'; then - pm2 delete all - pdone "Old processes stopped" -fi - -# Start neurons based on type -if [ "$NEURON_TYPE" = "validator" ] -then - ohai "Starting validator on network '$NETWORK' ..." - VALIDATOR_ARGS="--actual_batch_size 6 --wallet.name default --wallet.hotkey validator --bucket $BUCKET --use_wandb --project $PROJECT --netuid $NETUID --autoupdate --remote" - - # Add network options - [ -n "$PM2_NETWORK_OPTIONS" ] && VALIDATOR_ARGS="$VALIDATOR_ARGS $PM2_NETWORK_OPTIONS" - [ -n "$SUBTENSOR_NETWORK" ] && VALIDATOR_ARGS="$VALIDATOR_ARGS --subtensor.network $SUBTENSOR_NETWORK" - [ -n "$SUBTENSOR_CHAIN_ENDPOINT" ] && VALIDATOR_ARGS="$VALIDATOR_ARGS --subtensor.chain_endpoint $SUBTENSOR_CHAIN_ENDPOINT" - - # Start validator - if [ "$DEBUG" = "true" ] - then - execute pm2 start neurons/validator.py --interpreter python3 --name ${NETWORK}_validator -- $VALIDATOR_ARGS - else - execute pm2 start neurons/validator.py --interpreter python3 --name ${NETWORK}_validator -- $VALIDATOR_ARGS > /dev/null 2>&1 - fi - pdone "Validator started" - LOGGING_TARGET="${NETWORK}_validator" -fi - -if [ "$NEURON_TYPE" = "miner" ] -then - ohai "Starting miners on network '$NETWORK' ..." - if [ "$NUM_GPUS" -gt 0 ] - then - for i in $(seq 0 $((NUM_GPUS - 1))) - do - GPU_INDEX=$i - HOTKEY_NAME="C$i" - GPU_MEMORY=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | sed -n "$((i + 1))p") - - if [ -z "$GPU_MEMORY" ] - then - warn "Could not get GPU memory for GPU $i" - continue - fi - - # Set batch size - if [ "$GPU_MEMORY" -ge 80000 ] - then - BATCH_SIZE=6 - elif [ "$GPU_MEMORY" -ge 40000 ] - then - BATCH_SIZE=3 - else - BATCH_SIZE=1 - fi - - ohai "Starting miner on GPU $GPU_INDEX with batch size $BATCH_SIZE..." - MINER_ARGS="--actual_batch_size $BATCH_SIZE --wallet.name default --wallet.hotkey $HOTKEY_NAME --bucket $BUCKET --device cuda:$GPU_INDEX --use_wandb --project $PROJECT --netuid $NETUID --remote" - - # Add network options - [ -n "$PM2_NETWORK_OPTIONS" ] && MINER_ARGS="$MINER_ARGS $PM2_NETWORK_OPTIONS" - [ -n "$SUBTENSOR_NETWORK" ] && MINER_ARGS="$MINER_ARGS --subtensor.network $SUBTENSOR_NETWORK" - [ -n "$SUBTENSOR_CHAIN_ENDPOINT" ] && MINER_ARGS="$MINER_ARGS --subtensor.chain_endpoint $SUBTENSOR_CHAIN_ENDPOINT" - - # Start miner - if [ "$DEBUG" = "true" ] - then - execute pm2 start neurons/miner.py --interpreter python3 --name ${NETWORK}_$HOTKEY_NAME -- $MINER_ARGS - else - execute pm2 start neurons/miner.py --interpreter python3 --name ${NETWORK}_$HOTKEY_NAME -- $MINER_ARGS > /dev/null 2>&1 - fi - done - LOGGING_TARGET="${NETWORK}_C0" - else - warn "No GPUs found. Skipping miner startup." - fi - pdone "All miners started" -fi - -# Display status -pm2 list -echo "" -pdone "SUCCESS" -echo "" - -# Start logs -pm2 logs $LOGGING_TARGET \ No newline at end of file diff --git a/scripts/start.sh b/scripts/start.sh index 73ed692..592748c 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1,35 +1,4 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Close down all previous processes and restart them. -pm2 sendSignal SIGINT all -pm2 delete all -# Delete items from bucket -BUCKET=${1:-cont2} -PROJECT=${2:-templar} -python3 tools/clean.py --bucket $BUCKET - -# Start all the processes again. -pm2 start neurons/validator.py --interpreter python3 --name V2 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey V1 --bucket $BUCKET --device cuda:0 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name V2 -pm2 start neurons/miner.py --interpreter python3 --name M1 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M111 --bucket $BUCKET --device cuda:1 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M1 -pm2 start neurons/miner.py --interpreter python3 --name M2 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M222 --bucket $BUCKET --device cuda:2 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M2 -pm2 start neurons/miner.py --interpreter python3 --name M3 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M333 --bucket $BUCKET --device cuda:3 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M3 -pm2 start neurons/miner.py --interpreter python3 --name M4 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M444 --bucket $BUCKET --device cuda:4 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M4 -pm2 start neurons/miner.py --interpreter python3 --name M5 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M555 --bucket $BUCKET --device cuda:5 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M5 -pm2 start neurons/miner.py --interpreter python3 --name M6 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M666 --bucket $BUCKET --device cuda:6 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M6 - -pm2 start neurons/validator.py --interpreter python3 --name V1 -- --actual_batch_size 6 --wallet.name templar --wallet.hotkey templar_validator --device cuda:7 --use_wandb --netuid 3 --autoupdate --process_name V1 --sync +# pm2 delete all +pm2 start neurons/miner.py --interpreter python3 --name TM0 -- --wallet.name Bistro --wallet.hotkey M111 --device cuda:0 --subtensor.network test --use_wandb --debug --netuid 268 --project ${1:-default} +pm2 start neurons/miner.py --interpreter python3 --name TM1 -- --wallet.name Bistro --wallet.hotkey M222 --device cuda:1 --subtensor.network test --use_wandb --debug --netuid 268 --project ${1:-default} +pm2 start neurons/validator.py --interpreter python3 --name TV1 -- --wallet.name Bistro --wallet.hotkey V11 --device cuda:3 --subtensor.network test --use_wandb --debug --netuid 268 --project ${1:-default} diff --git a/src/templar/autoupdate.py b/src/templar/autoupdate.py deleted file mode 100644 index 5c176f2..0000000 --- a/src/templar/autoupdate.py +++ /dev/null @@ -1,327 +0,0 @@ -# Global imports -import asyncio -import aiohttp -from packaging import version -import git -import os -import subprocess -import sys -import threading -import time -import json - -# Local imports -from .config import BUCKET_SECRETS -from .comms import delete_old_version_files -from .logging import logger - - -TARGET_BRANCH = "main" - - -class AutoUpdate(threading.Thread): - """ - Automatic update utility for templar neurons. - """ - - def __init__(self): - super().__init__() - self.daemon = True # Ensure thread exits when main program exits - try: - self.repo = git.Repo(search_parent_directories=True) - except Exception as e: - logger.exception("Failed to initialize the repository", exc_info=e) - sys.exit(1) # Terminate the thread/application - - async def get_remote_version(self): - """ - Asynchronously fetch the remote version string from a remote HTTP endpoint. - """ - try: - url = "https://raw.githubusercontent.com/tplr-ai/templar/main/src/templar/__init__.py" - async with aiohttp.ClientSession() as session: - async with session.get(url, timeout=5) as response: - response.raise_for_status() - content = await response.text() - - for line in content.split("\n"): - if line.startswith("__version__"): - version_info = line.split("=")[1].strip().strip(" \"'") - return version_info - - logger.error("Version string not found in remote __init__.py") - return None - - except Exception as e: - logger.exception( - "Failed to get remote version for version check", exc_info=e - ) - return None - - async def check_version_updated(self): - """ - Asynchronously compares local and remote versions and returns True if the remote version is higher. - """ - remote_version = await self.get_remote_version() - if not remote_version: - logger.error("Failed to get remote version, skipping version check") - return False - - local_version = self.get_local_version() - if not local_version: - logger.error("Failed to get local version, skipping version check") - return False - - local_version_obj = version.parse(local_version) - remote_version_obj = version.parse(remote_version) - logger.info( - f"Version check - remote_version: {remote_version}, local_version: {local_version}" - ) - - if remote_version_obj > local_version_obj: - logger.info( - f"Remote version ({remote_version}) is higher " - f"than local version ({local_version}), automatically updating..." - ) - return True - - return False - - def attempt_update(self): - """ - Attempts to update the local repository to match the remote. - """ - if self.repo.head.is_detached: - logger.error("Repository is in a detached HEAD state. Cannot update.") - return False - - if self.repo.is_dirty(untracked_files=True): - logger.error( - "Repository has uncommitted changes or untracked files. Cannot update." - ) - return False - - try: - origin = self.repo.remote(name="origin") - # Fetch latest changes from remote - origin.fetch() - # Get the current branch - current_branch = self.repo.active_branch - if current_branch.name != TARGET_BRANCH: - logger.error( - f"Current branch ({current_branch.name}) is not the target branch ({TARGET_BRANCH}). Cannot update." - ) - return False - - # Reset local branch to the remote branch - remote_ref = f"origin/{TARGET_BRANCH}" - logger.info( - f"Resetting local branch '{current_branch.name}' to '{remote_ref}'" - ) - self.repo.git.reset("--hard", remote_ref) - logger.info("Successfully reset to the latest commit from remote.") - - # Verify that local and remote commits match - local_commit = self.repo.commit(current_branch) - remote_commit = self.repo.commit(remote_ref) - if local_commit.hexsha != remote_commit.hexsha: - logger.error( - "Local commit does not match remote commit after reset. Rolling back." - ) - self.repo.git.reset("--hard", "HEAD@{1}") # Reset to previous HEAD - return False - - return True - except git.exc.GitCommandError as e: - logger.error(f"Git command failed: {e}") - # Rollback on failure - self.repo.git.reset("--hard", "HEAD@{1}") - return False - except Exception as e: - logger.exception("Failed to update repository.", exc_info=e) - return False - except git.exc.GitCommandError as e: - logger.error(f"Git command failed: {e}") - return False - except Exception as e: - logger.exception("Failed to update repository.", exc_info=e) - return False - - def handle_merge_conflicts(self): - """ - Attempt to automatically resolve any merge conflicts that may have arisen. - """ - try: - self.repo.git.reset("--merge") - origin = self.repo.remote(name="origin") - current_branch = self.repo.active_branch.name - origin.pull(current_branch) - - for item in self.repo.index.diff(None): - file_path = item.a_path - logger.info(f"Resolving conflict in file: {file_path}") - self.repo.git.checkout("--theirs", file_path) - self.repo.index.commit("Resolved merge conflicts automatically") - logger.info("Merge conflicts resolved, repository updated to remote state.") - logger.info("✅ Successfully updated") - return True - except git.GitCommandError as e: - logger.exception( - "Failed to resolve merge conflicts. Please manually pull and update.", - exc_info=e, - ) - return False - - def attempt_package_update(self): - """ - Synchronize dependencies using 'uv sync --extra all'. - """ - logger.info("Attempting to update packages using 'uv sync --extra all'...") - - try: - uv_executable = "uv" - # TODO: Allow specifying the path to 'uv' if it's not in PATH - - subprocess.check_call( - [uv_executable, "sync", "--extra", "all"], - timeout=300, - ) - logger.info("Successfully updated packages using 'uv sync --extra all'.") - except subprocess.CalledProcessError as e: - logger.exception("Failed to synchronize dependencies with uv", exc_info=e) - except FileNotFoundError: - logger.error( - "uv executable not found. Please ensure 'uv' is installed and in PATH." - ) - except Exception as e: - logger.exception( - "Unexpected error during package synchronization", exc_info=e - ) - - async def cleanup_old_versions(self): - """ - Cleans up old version slices from the S3 bucket. - """ - from templar import __version__ - - logger.info( - f"Cleaning up old versions from bucket {BUCKET_SECRETS['bucket_name']}" - ) - await delete_old_version_files(BUCKET_SECRETS["bucket_name"], __version__) - - def try_update(self): - """ - Automatic update entrypoint method. - """ - - if self.repo.head.is_detached or self.repo.active_branch.name != TARGET_BRANCH: - logger.info("Not on the target branch, skipping auto-update") - return - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - logger.info("Checking for updates...") - # Check if remote version is newer - is_update_needed = loop.run_until_complete(self.check_version_updated()) - if not is_update_needed: - logger.info("Local version is up to date. No updates needed.") - return - - logger.info("Attempting auto update") - # Attempt to update code - update_applied = self.attempt_update() - if not update_applied: - logger.info("No updates were applied. Continuing without restart.") - return - - # Now read the local version - local_version = self.get_local_version() - logger.info(f"Local version after update: {local_version}") - - # Synchronize dependencies - self.attempt_package_update() - - # Clean up old versions from the bucket - loop.run_until_complete(self.cleanup_old_versions()) - - # Restart application - logger.info("Attempting to restart the application...") - self.restart_app() - except Exception as e: - logger.exception("Exception during autoupdate process", exc_info=e) - finally: - loop.close() - - def get_pm2_process_name(self): - """ - Attempt to find the current process's PM2 name by using `pm2 jlist` and matching the current PID. - """ - current_pid = os.getpid() - try: - result = subprocess.run( - ["pm2", "jlist"], check=True, capture_output=True, text=True - ) - pm2_data = json.loads(result.stdout) - except Exception as e: - logger.error(f"Error running `pm2 jlist`: {e}") - return None - for proc in pm2_data: - if proc.get("pid") == current_pid: - return proc.get("name") - - return None - - def restart_app(self): - """Restarts the current application appropriately based on the runtime environment.""" - logger.info("Restarting application...") - pm2_name = self.get_pm2_process_name() - if pm2_name: - logger.info( - f"Detected PM2 environment. Restarting PM2 process '{pm2_name}'..." - ) - try: - subprocess.run(["pm2", "restart", pm2_name], check=True) - logger.info(f"Successfully restarted PM2 process '{pm2_name}'.") - sys.exit(0) - except Exception as e: - logger.error(f"Failed to restart PM2 process '{pm2_name}': {e}") - sys.exit(1) - else: - try: - logger.info( - "PM2 process name not found. Performing regular restart using subprocess.Popen" - ) - subprocess.Popen([sys.executable] + sys.argv) - logger.info("New process started. Exiting current process.") - sys.exit(0) - except Exception as e: - logger.exception("Failed to restart application.", exc_info=e) - sys.exit(1) - - def run(self): - """Thread run method to periodically check for updates.""" - while True: - try: - logger.info("Running autoupdate") - self.try_update() - except Exception as e: - logger.exception("Exception during autoupdate check", exc_info=e) - time.sleep(60) - - def get_local_version(self): - """ - Reads the local __version__ from the __init__.py file. - """ - try: - init_py_path = os.path.join(os.path.dirname(__file__), "__init__.py") - with open(init_py_path, "r") as f: - content = f.read() - for line in content.split("\n"): - if line.startswith("__version__"): - local_version = line.split("=")[1].strip().strip(" \"'") - return local_version - logger.error("Could not find __version__ in local __init__.py") - return None - except Exception as e: - logger.exception("Failed to read local version", exc_info=e) - return None diff --git a/src/templar/chain.py b/src/templar/chain.py deleted file mode 100644 index 00015f6..0000000 --- a/src/templar/chain.py +++ /dev/null @@ -1,118 +0,0 @@ -# Global imports -import bittensor as bt -from pydantic import ValidationError - -# Local imports -import templar as tplr -from templar.schemas import Bucket - - -class ChainManager: - def __init__(self, subtensor: bt.Subtensor, wallet: bt.wallet, netuid: int): - """Class used to get commits from and send commits to the blockchain. - - Args: - subtensor: Subtensor network object. - wallet: The wallet associated with the neuron committing the data. - netuid: The unique identifier of the subnetwork. - """ - self.subtensor = subtensor - self.wallet = wallet - self.netuid = netuid - - def commit(self) -> None: - """Commits bucket configuration data to the subtensor network. - - This method prepares and commits bucket configuration data associated - with the wallet to the subtensor network. The data includes: - - Account ID: A string of fixed length 32 characters. - - Access key ID: A string of fixed length 32 characters. - - Secret access key: A string of variable length (up to 64 characters). - - The commitment process involves: - - Fetching the required configuration details from the `tplr.config.BUCKET_SECRETS` - dictionary. - - Concatenating the account ID, access key ID, and secret access key - into a single string, in this exact order. - - Committing the concatenated data to the subtensor network using the - configured `netuid` and wallet. - - **Note:** The order of concatenation (account ID, access key ID, secret - access key) is critical for correct parsing when the data is retrieved. - - Logs provide visibility into the data type and structure before - committing. - - Raises: - Any exceptions that might arise from the subtensor network - communication are propagated. - """ - concatenated = ( - tplr.config.BUCKET_SECRETS["account_id"] - + tplr.config.BUCKET_SECRETS["read"]["access_key_id"] - + tplr.config.BUCKET_SECRETS["read"]["secret_access_key"] - ) - self.subtensor.commit(self.wallet, self.netuid, concatenated) - tplr.logger.info( - f"Committed {type(concatenated)} data of type to the network: {concatenated}" - ) - - def get_commitment(self, uid: int) -> Bucket: - """Retrieves and parses committed bucket configuration data for a given - UID. - - This method fetches commitment data for a specific UID from the - subtensor network and decodes it into a structured format. The - retrieved data is split into the following fields: - - Account ID: A string of fixed length 32 characters. - - Access key ID: A string of fixed length 32 characters. - - Secret access key: A string of variable length (up to 64 characters). - - The parsed fields are then mapped to an instance of the `Bucket` class. - When initializing the Bucket object, the account ID is also used as the - bucket name. - - The retrieval process involves: - - Fetching the commitment data for the specified UID using the - configured `netuid` from the subtensor network. - - Splitting the concatenated string into individual fields based on - their expected lengths and order. - - Mapping the parsed fields to a `Bucket` instance. - - **Note:** The order of fields (bucket name, account ID, access key ID, - secret access key) in the concatenated string is critical for accurate - parsing. - - Args: - uid: The UID of the neuron whose commitment data is being - retrieved. - - Returns: - Bucket: An instance of the `Bucket` class containing the parsed - bucket configuration details. - - Raises: - ValueError: If the parsed data does not conform to the expected - format for the `Bucket` class. - Exception: If an error occurs while retrieving the commitment data - from the subtensor network. - """ - try: - concatenated = self.subtensor.get_commitment(self.netuid, uid) - tplr.logger.success(f"Commitment fetched: {concatenated}") - except Exception as e: - raise Exception(f"Couldn't get commitment from uid {uid} because {e}") - if len(concatenated) != 128: - raise ValueError( - f"Commitment '{concatenated}' is of length {len(concatenated)} but should be of length 128." - ) - - try: - return Bucket( - name=concatenated[:32], - account_id=concatenated[:32], - access_key_id=concatenated[32:64], - secret_access_key=concatenated[64:], - ) - except ValidationError as e: - raise ValueError(f"Invalid data in commitment: {e}") diff --git a/src/templar/checkpoint.py b/src/templar/checkpoint.py deleted file mode 100644 index cd44617..0000000 --- a/src/templar/checkpoint.py +++ /dev/null @@ -1,1103 +0,0 @@ -import asyncio -import time -import aiofiles -import torch -import os -import glob -import re -import shutil -from typing import List, Optional, Union -from aiobotocore.session import get_session -from tqdm import tqdm -from . import __version__ -from . import config # Import config module -from .config import client_config -from .constants import CF_REGION_NAME -from .logging import logger -from .schemas import Bucket -from .commitment import get_all_commitments - - -def get_base_url(account_id: str) -> str: - """Get base URL for R2 storage""" - return f"https://{account_id}.r2.cloudflarestorage.com" - - -async def save_checkpoint( - filename: str, - model: torch.nn.Module, - optimizer: torch.optim.Optimizer = None, - scheduler: torch.optim.lr_scheduler._LRScheduler = None, - global_step: int = 0, - **kwargs, -): - """ - Saves the checkpoint to the specified filename asynchronously. - Uses asyncio.to_thread to avoid blocking the main event loop. - """ - checkpoint = { - "global_step": global_step, - "model_state_dict": model.state_dict(), - } - if optimizer: - checkpoint["optimizer_state_dict"] = optimizer.state_dict() - if scheduler: - checkpoint["scheduler_state_dict"] = scheduler.state_dict() - # Include additional state variables - checkpoint.update(kwargs) - - try: - await asyncio.to_thread(torch.save, checkpoint, filename) - logger.info(f"Checkpoint saved at {filename}") - except Exception as e: - logger.error(f"Failed to save checkpoint at {filename}: {e}") - raise - - -async def load_checkpoint( - filename: str, - model: torch.nn.Module, - optimizer: torch.optim.Optimizer, - scheduler: torch.optim.lr_scheduler._LRScheduler, - device: str = "cpu", - is_validator: bool = False, - hparams=None, -) -> int: - """ - Loads the checkpoint from the specified filename asynchronously. - Adjusts optimizer and scheduler for miners. - """ - try: - logger.info(f"Loading checkpoint from {filename}") - checkpoint = await asyncio.to_thread( - torch.load, filename, map_location=device, weights_only=True - ) - - # Load the model state - model.load_state_dict(checkpoint["model_state_dict"]) - global_step = checkpoint.get("global_step", 0) - logger.info(f"Loaded model state at global step {global_step}") - - # Load optimizer state - optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) - - # Adjust optimizer state if miner - if not is_validator: - # Retrieve validator's learning rate from optimizer state - validator_lr = optimizer.param_groups[0]["lr"] - miner_lr = hparams.learning_rate # Miner's learning rate - - # Compute scaling factor - scaling_factor = validator_lr / miner_lr - - # Scale optimizer's internal states - for state in optimizer.state.values(): - if "exp_avg" in state: - state["exp_avg"].mul_(scaling_factor) - if "exp_avg_sq" in state: - # Optionally adjust exp_avg_sq if needed - pass - - # Update optimizer's learning rate to miner's learning rate - for param_group in optimizer.param_groups: - param_group["lr"] = miner_lr - - logger.info("Adjusted optimizer states for miner.") - - else: - logger.info("Loaded optimizer states for validator.") - - return global_step - - except Exception as e: - logger.error(f"Failed to load checkpoint from {filename}: {e}") - return 0 - - -async def download_checkpoint_from_neuron( - bucket_info: Bucket, - neuron_hotkey: str, - checkpoint_dir: str, -) -> Optional[str]: - """ - Downloads the latest checkpoint file with parallel processing and progress tracking. - Handles multiple processes and provides detailed progress information. - """ - start_time = time.time() - regex_pattern = ( - rf"neuron_checkpoint_{neuron_hotkey}_b(\d+)_v({re.escape(__version__)})\.pth" - ) - local_checkpoint_path = None - chunk_size = 8 * 1024 * 1024 # 8MB chunks - max_concurrent_downloads = 4 - max_retries = 3 - retry_delay = 5 - - # Ensure checkpoint directory exists with absolute path - checkpoint_dir = os.path.abspath(checkpoint_dir) - os.makedirs(checkpoint_dir, exist_ok=True) - - def format_size(size_bytes): - """Convert bytes to human readable format""" - for unit in ["B", "KB", "MB", "GB", "TB"]: - if size_bytes < 1024.0: - return f"{size_bytes:.2f} {unit}" - size_bytes /= 1024.0 - - def create_progress_bar(progress, total_size): - """Create a progress bar with size information""" - width = 50 - filled = int(width * progress / 100) - bar = "█" * filled + "-" * (width - filled) - size_info = ( - f"{format_size(total_size * progress / 100)}/{format_size(total_size)}" - ) - return f"[{bar}] {progress:.1f}% {size_info}" - - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(bucket_info.account_id), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=bucket_info.access_key_id, - aws_secret_access_key=bucket_info.secret_access_key, - ) as s3_client: - try: - # Find latest checkpoint - paginator = s3_client.get_paginator("list_objects_v2") - latest_block_number = -1 - latest_filename = None - file_size = None - - async for page in paginator.paginate( - Bucket=bucket_info.name, Prefix=f"neuron_checkpoint_{neuron_hotkey}_" - ): - for obj in page.get("Contents", []): - key = obj["Key"] - match = re.match(regex_pattern, key) - if match: - block_number = int(match.group(1)) - if block_number > latest_block_number: - latest_block_number = block_number - latest_filename = key - file_size = obj["Size"] - - if not latest_filename: - logger.info(f"No valid checkpoints found for neuron {neuron_hotkey}") - return None - - logger.info( - f"Found latest checkpoint: {latest_filename} ({format_size(file_size)})" - ) - local_checkpoint_path = os.path.join(checkpoint_dir, latest_filename) - temp_path = f"{local_checkpoint_path}.temp" - lock_path = f"{local_checkpoint_path}.lock" - - # Check if file already exists and is complete - if os.path.exists(local_checkpoint_path): - if os.path.getsize(local_checkpoint_path) == file_size: - logger.info( - f"Checkpoint already exists and is complete: {local_checkpoint_path}" - ) - return local_checkpoint_path - - # Try to acquire lock - try: - with open(lock_path, "x") as _: # Atomic file creation - logger.info(f"Acquired lock for downloading: {lock_path}") - except FileExistsError: - # Another process is downloading, wait for it - logger.info("Another process is downloading, waiting...") - for _ in range(30): # Wait up to 30 seconds - await asyncio.sleep(1) - if os.path.exists(local_checkpoint_path): - if os.path.getsize(local_checkpoint_path) == file_size: - logger.info("File downloaded by another process") - try: - os.remove(lock_path) # Try to clean up lock - except OSError as e: - logger.warning(f"Failed to remove lock file: {e}") - return local_checkpoint_path - logger.warning( - "Timeout waiting for other process, proceeding with download" - ) - - try: - # Download chunks - chunks_data = {} - downloaded_size = 0 - semaphore = asyncio.Semaphore(max_concurrent_downloads) - total_chunks = (file_size + chunk_size - 1) // chunk_size - - async def download_chunk(chunk_number: int): - start = chunk_number * chunk_size - end = min(start + chunk_size, file_size) - - for attempt in range(max_retries): - try: - async with semaphore: - response = await s3_client.get_object( - Bucket=bucket_info.name, - Key=latest_filename, - Range=f"bytes={start}-{end-1}", - ) - chunk_data = await response["Body"].read() - - nonlocal downloaded_size - downloaded_size += len(chunk_data) - progress = (downloaded_size / file_size) * 100 - - if chunk_number % 5 == 0 or progress >= 100: - elapsed_time = time.time() - start_time - speed = downloaded_size / ( - 1024 * 1024 * elapsed_time - ) # MB/s - progress_bar = create_progress_bar( - progress, file_size - ) - logger.info( - f"\nDownload Progress: {progress_bar} [{speed:.2f} MB/s]" - ) - - chunks_data[chunk_number] = chunk_data - return True - - except Exception as e: - if attempt == max_retries - 1: - logger.error( - f"Failed to download chunk {chunk_number}: {str(e)}" - ) - return False - await asyncio.sleep(retry_delay * (attempt + 1)) - - # Download all chunks - tasks = [download_chunk(i) for i in range(total_chunks)] - results = await asyncio.gather(*tasks) - - if not all(results): - raise Exception("Some chunks failed to download") - - # Write chunks to temp file - logger.info("Writing chunks to temp file...") - async with aiofiles.open(temp_path, "wb") as f: - for chunk_num in range(total_chunks): - if chunk_num in chunks_data: - await f.write(chunks_data[chunk_num]) - else: - raise Exception(f"Missing chunk {chunk_num}") - - await asyncio.sleep(0.5) # Short delay for file system - - # Verify the temp file - if not os.path.exists(temp_path): - raise Exception(f"Temp file not found at: {temp_path}") - - actual_size = os.path.getsize(temp_path) - if actual_size != file_size: - raise Exception( - f"Size mismatch in temp file: expected {file_size}, got {actual_size}" - ) - - # Move to final location with extra verification - logger.info( - f"Moving temp file to final location: {local_checkpoint_path}" - ) - - # Remove destination file if it exists - if os.path.exists(local_checkpoint_path): - logger.info( - f"Removing existing checkpoint file: {local_checkpoint_path}" - ) - os.remove(local_checkpoint_path) - - try: - # Use shutil.move for more reliable cross-device moves - shutil.move(temp_path, local_checkpoint_path) - - # Verify the move - if not os.path.exists(local_checkpoint_path): - raise Exception( - "Move operation failed - destination file doesn't exist" - ) - - # Double check the source file is gone - if os.path.exists(temp_path): - logger.warning( - "Temp file still exists after move, attempting cleanup" - ) - try: - os.remove(temp_path) - except Exception as e: - logger.warning(f"Failed to cleanup temp file: {e}") - - # Final size verification - final_size = os.path.getsize(local_checkpoint_path) - if final_size != file_size: - raise Exception( - f"Size mismatch in final file: expected {file_size}, got {final_size}" - ) - - # Extra verification - try to open the file - with open(local_checkpoint_path, "rb") as f: - # Read first few bytes to verify file is accessible - f.read(1024) - - logger.info("Move operation successful and verified") - - total_time = time.time() - start_time - avg_speed = (file_size / (1024 * 1024)) / total_time # MB/s - logger.info( - f"Successfully downloaded checkpoint to: {local_checkpoint_path}" - ) - logger.info( - f"Download completed in {total_time:.2f} seconds ({avg_speed:.2f} MB/s average)" - ) - - return local_checkpoint_path - - except Exception as move_e: - logger.error(f"Error during move operation: {str(move_e)}") - # Try to recover the temp file if move failed - if os.path.exists(temp_path) and not os.path.exists( - local_checkpoint_path - ): - try: - shutil.copy2(temp_path, local_checkpoint_path) - logger.info("Recovered file using copy operation") - except Exception as recover_e: - logger.error(f"Failed to recover file: {str(recover_e)}") - raise - - except Exception as e: - logger.error(f"Error during file operations: {str(e)}") - # Cleanup both temp and final files if they exist - for filepath in [temp_path, local_checkpoint_path]: - if filepath and os.path.exists(filepath): - try: - os.remove(filepath) - logger.info(f"Cleaned up file: {filepath}") - except Exception as rm_e: - logger.error( - f"Failed to cleanup file {filepath}: {str(rm_e)}" - ) - return None - - finally: - # Clean up lock file - try: - os.remove(lock_path) - except Exception as e: - logger.warning(f"Failed to remove lock file: {str(e)}") - - except Exception as e: - logger.error(f"Unexpected error: {str(e)}") - return None - - -def get_all_buckets( - netuid: int, - metagraph, - config, -) -> List[Optional[Union[str, Bucket]]]: - """ - Retrieves and parses all bucket commitments from the network. - """ - buckets = [] - commitments = get_all_commitments( - netuid=netuid, - metagraph=metagraph, - config=config, - ) - - for uid in metagraph.uids: - bucket = commitments.get(uid) - logger.debug(f"UID {uid} bucket: {bucket}") - - if bucket is not None: - logger.debug(f"Retrieved valid bucket for UID {uid}: {bucket}") - buckets.append(bucket) - else: - logger.debug(f"No valid bucket found for UID {uid}") - buckets.append(None) - - logger.debug(f"Final list of buckets: {buckets}") - return buckets - - -def get_neuron_with_highest_stake( - metagraph, buckets: List[Optional[Union[str, Bucket]]] -) -> Optional[str]: - """ - Get the hotkey of the neuron with highest stake that has a valid bucket. - """ - try: - highest_stake_uid = int(metagraph.S.argmax()) - if highest_stake_uid < len(buckets) and buckets[highest_stake_uid] is not None: - return metagraph.hotkeys[highest_stake_uid] - logger.warning("No valid bucket found for highest stake neuron") - return None - except Exception as e: - logger.error(f"Error finding highest stake neuron: {e}") - return None - - -async def load_highest_stake_checkpoint( - metagraph, - buckets: List[Optional[Union[str, Bucket]]], - model: torch.nn.Module, - checkpoint_path: str, - device: str = "cpu", - optimizer: Optional[torch.optim.Optimizer] = None, - scheduler: Optional[torch.optim.lr_scheduler._LRScheduler] = None, -) -> int: - """ - Attempts to load checkpoint from the highest stake neuron. - """ - try: - highest_stake_hotkey = get_neuron_with_highest_stake( - metagraph=metagraph, buckets=buckets - ) - - if highest_stake_hotkey: - uid = metagraph.hotkeys.index(highest_stake_hotkey) - bucket_info = buckets[uid] - - if bucket_info: - checkpoint_dir = os.path.dirname(checkpoint_path) - await asyncio.to_thread(os.makedirs, checkpoint_dir, exist_ok=True) - - checkpoint_file = await download_checkpoint_from_neuron( - bucket_info=bucket_info, - neuron_hotkey=highest_stake_hotkey, - checkpoint_dir=checkpoint_dir, - ) - - if checkpoint_file: - global_step, _ = await load_checkpoint( - filename=checkpoint_file, - model=model, - device=device, - optimizer=optimizer, - scheduler=scheduler, - ) - logger.info(f"Resumed from global step {global_step}") - return global_step if global_step is not None else 0 - - logger.warning( - "Failed to download neuron checkpoint. Starting from scratch." - ) - return 0 - - logger.warning( - f"No bucket info for neuron {highest_stake_hotkey}. Starting from scratch." - ) - return 0 - - logger.warning("No neurons found. Starting from scratch.") - return 0 - - except Exception as e: - logger.error(f"Error loading checkpoint: {e}") - return 0 - - -class CheckpointManager: - """ - Improved CheckpointManager that saves and uploads checkpoints asynchronously, - and can clean up old checkpoints, all without blocking the main thread. - """ - - def __init__( - self, - model: torch.nn.Module, - checkpoint_path: str, - wallet, - device: str = "cpu", - optimizer: Optional[torch.optim.Optimizer] = None, - scheduler: Optional[torch.optim.lr_scheduler._LRScheduler] = None, - ): - self.model = model - self.checkpoint_path = checkpoint_path - self.wallet = wallet - self.device = device - self.optimizer = optimizer - self.scheduler = scheduler - self.upload_task = None # Track the upload task - - self.checkpoint_dir = os.path.dirname(self.checkpoint_path) or os.getcwd() - os.makedirs(self.checkpoint_dir, exist_ok=True) - - self._shutdown = False - - async def _save_checkpoint_async( - self, global_step: int, block_number: int, **kwargs - ): - """Asynchronously save a checkpoint.""" - checkpoint = { - "global_step": global_step, - "block_number": block_number, - "model_state_dict": self.model.state_dict(), - } - - if self.optimizer: - checkpoint["optimizer_state_dict"] = self.optimizer.state_dict() - if self.scheduler: - checkpoint["scheduler_state_dict"] = self.scheduler.state_dict() - - checkpoint.update(kwargs) - - filename = f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b{block_number}_v{__version__}.pth" - full_path = os.path.join(self.checkpoint_dir, filename) - - await asyncio.to_thread(torch.save, checkpoint, full_path) - self.checkpoint_path = full_path - logger.info(f"Checkpoint saved at {self.checkpoint_path}") - - async def _upload_checkpoint_async(self): - """Async checkpoint upload to S3 with verified parallel uploads.""" - try: - filename = os.path.basename(self.checkpoint_path) - logger.info(f"Starting checkpoint upload to S3: {filename}") - - bucket = config.BUCKET_SECRETS["bucket_name"].split("/")[-1] - chunk_size = 16 * 1024 * 1024 # 16MB chunks - max_concurrent_uploads = 50 - max_retries = 3 - retry_delay = 5 - - session = get_session() - async with session.create_client( - "s3", - endpoint_url=f"https://{config.BUCKET_SECRETS['account_id']}.r2.cloudflarestorage.com", - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=config.BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=config.BUCKET_SECRETS["write"][ - "secret_access_key" - ], - ) as s3_client: - # Initialize multipart upload - response = await s3_client.create_multipart_upload( - Bucket=bucket, - Key=filename, - CacheControl="no-cache, no-store, must-revalidate", - ) - upload_id = response["UploadId"] - logger.info(f"Initiated multipart upload with ID: {upload_id}") - - try: - total_size = os.path.getsize(self.checkpoint_path) - total_parts = (total_size + chunk_size - 1) // chunk_size - parts = {} # Use dict to track parts by number - uploaded_size = 0 - semaphore = asyncio.Semaphore(max_concurrent_uploads) - upload_tasks = [] - failed_parts = set() - - # Initialize progress bar - pbar = tqdm( - total=total_size, - unit="B", - unit_scale=True, - desc="Uploading checkpoint", - ) - - async def upload_part_with_retry( - part_number: int, offset: int - ) -> dict: - """Upload a single part with retries and verification.""" - for attempt in range(max_retries): - try: - async with semaphore: - async with aiofiles.open( - self.checkpoint_path, "rb" - ) as f: - await f.seek(offset) - chunk = await f.read( - min(chunk_size, total_size - offset) - ) - - response = await s3_client.upload_part( - Bucket=bucket, - Key=filename, - PartNumber=part_number, - UploadId=upload_id, - Body=chunk, - ) - - # Verify part upload - part_size = len(chunk) - if part_size == 0: - raise ValueError( - f"Zero-size chunk for part {part_number}" - ) - - pbar.update(part_size) - - return { - "PartNumber": part_number, - "ETag": response["ETag"], - "Size": part_size, - } - except Exception as e: - if attempt < max_retries - 1: - logger.warning( - f"Retry {attempt + 1}/{max_retries} for part {part_number}: {str(e)}" - ) - await asyncio.sleep(retry_delay) - else: - failed_parts.add(part_number) - raise - - # Create upload tasks for all parts - for part_number in range(1, total_parts + 1): - offset = (part_number - 1) * chunk_size - task = asyncio.create_task( - upload_part_with_retry(part_number, offset) - ) - upload_tasks.append(task) - - # Wait for all uploads and collect results - completed_parts = await asyncio.gather( - *upload_tasks, return_exceptions=True - ) - - # Close progress bar - pbar.close() - - # Process results and check for failures - for part in completed_parts: - if isinstance(part, Exception): - raise Exception(f"Part upload failed: {str(part)}") - parts[part["PartNumber"]] = part - - # Verify all parts are present and ordered - if len(parts) != total_parts: - missing_parts = set(range(1, total_parts + 1)) - set( - parts.keys() - ) - raise Exception(f"Missing parts: {missing_parts}") - - # Sort parts for completion - ordered_parts = [parts[i] for i in range(1, total_parts + 1)] - - # Complete multipart upload - completion_response = await s3_client.complete_multipart_upload( - Bucket=bucket, - Key=filename, - UploadId=upload_id, - MultipartUpload={ - "Parts": [ - {"PartNumber": p["PartNumber"], "ETag": p["ETag"]} - for p in ordered_parts - ] - }, - ) - - # Verify upload completion - try: - head_response = await s3_client.head_object( - Bucket=bucket, Key=filename - ) - if head_response["ContentLength"] != total_size: - raise Exception( - f"Size mismatch: uploaded={head_response['ContentLength']}, expected={total_size}" - ) - - logger.info( - f"Successfully verified upload of {filename} ({total_size} bytes)" - ) - except Exception as e: - raise Exception(f"Upload verification failed: {str(e)}") - - except Exception as e: - logger.error(f"Error during upload: {str(e)}") - try: - await s3_client.abort_multipart_upload( - Bucket=bucket, Key=filename, UploadId=upload_id - ) - logger.info(f"Aborted multipart upload {upload_id}") - except Exception as abort_e: - logger.error( - f"Failed to abort multipart upload: {str(abort_e)}" - ) - raise - - except Exception as e: - logger.exception(f"Failed to upload checkpoint: {e}") - raise - - finally: - # Clean up any remaining tasks - if "upload_tasks" in locals(): - for task in upload_tasks: - if not task.done(): - task.cancel() - - async def _cleanup_old_checkpoints_async(self, max_checkpoints=3): - """ - Asynchronously deletes old checkpoints locally and in S3. - Keeps only the latest 'max_checkpoints'. - """ - logger.info( - f"Starting checkpoint cleanup, keeping latest {max_checkpoints} checkpoints" - ) - pattern = os.path.join( - self.checkpoint_dir, - f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b*_v{__version__}.pth", - ) - logger.info(f"Looking for checkpoints matching pattern: {pattern}") - - checkpoint_files = await asyncio.to_thread(glob.glob, pattern) - logger.info(f"Found {len(checkpoint_files)} total checkpoint files") - if len(checkpoint_files) <= max_checkpoints: - logger.info("No cleanup needed - number of checkpoints below threshold") - return - - # Parse block numbers - logger.info("Parsing block numbers from checkpoint filenames") - checkpoints = [] - for filepath in checkpoint_files: - filename = os.path.basename(filepath) - logger.info(f"Processing checkpoint file: {filename}") - match = re.match( - rf"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b(\d+)_v{__version__}\.pth", - filename, - ) - if match: - block_number = int(match.group(1)) - logger.info(f"Found checkpoint for block {block_number}") - checkpoints.append((block_number, filepath)) - - # Sort by block number descending - checkpoints.sort(reverse=True) - old_checkpoints = checkpoints[max_checkpoints:] - logger.info(f"Identified {len(old_checkpoints)} checkpoints to delete") - - # Delete local files - logger.info("Starting deletion of local checkpoint files") - for block_num, filepath in old_checkpoints: - try: - logger.info( - f"Attempting to delete checkpoint from block {block_num} at {filepath}" - ) - await asyncio.to_thread(os.remove, filepath) - logger.info(f"Successfully deleted local checkpoint: {filepath}") - except Exception as e: - logger.warning(f"Failed to delete local checkpoint {filepath}: {e}") - logger.error(f"Error details: {str(e)}") - - # Delete old checkpoints from S3 - logger.info("Starting deletion of S3 checkpoint files") - await self._delete_old_checkpoints_from_s3(old_checkpoints) - - async def _delete_old_checkpoints_from_s3(self, old_checkpoints): - logger.info(f"Starting S3 checkpoint deletion for {len(old_checkpoints)} files") - bucket = config.BUCKET_SECRETS["bucket_name"].split("/")[-1] - logger.info(f"Using bucket: {bucket}") - - session = get_session() - logger.info("Created aiobotocore session") - - logger.info( - f"Connecting to S3 endpoint: {get_base_url(config.BUCKET_SECRETS['account_id'])}" - ) - async with session.create_client( - "s3", - endpoint_url=get_base_url(config.BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=config.BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=config.BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - logger.info("Successfully connected to S3") - - delete_objects = { - "Objects": [ - {"Key": os.path.basename(filepath)} - for _, filepath in old_checkpoints - ], - "Quiet": True, - } - logger.info( - f"Prepared delete request for {len(delete_objects['Objects'])} objects" - ) - - if delete_objects["Objects"]: - try: - logger.info( - f"Attempting to delete objects: {[obj['Key'] for obj in delete_objects['Objects']]}" - ) - response = await s3_client.delete_objects( - Bucket=bucket, Delete=delete_objects - ) - logger.info("Successfully initiated deletion request") - logger.info( - f"Deleted old checkpoints from S3: {delete_objects['Objects']}" - ) - logger.info(f"S3 deletion response: {response}") - - if "Deleted" in response: - logger.info( - f"Successfully deleted {len(response['Deleted'])} objects" - ) - if "Errors" in response: - logger.warning( - f"Failed to delete {len(response['Errors'])} objects: {response['Errors']}" - ) - - except Exception as e: - logger.error(f"Failed to delete old checkpoints from S3: {str(e)}") - logger.error( - f"Full error details: {e.__class__.__name__}: {str(e)}" - ) - - async def load_from_highest_stake( - self, - metagraph, - buckets, - optimizer, - scheduler, - is_validator: bool = False, - hparams=None, - ) -> int: - """ - Attempts to load checkpoint from the highest stake neuron. - """ - try: - await self.cleanup_old_version_checkpoints() - highest_stake_hotkey = get_neuron_with_highest_stake( - metagraph=metagraph, buckets=buckets - ) - - if highest_stake_hotkey: - uid = metagraph.hotkeys.index(highest_stake_hotkey) - bucket_info = buckets[uid] - - if bucket_info: - checkpoint_dir = os.path.dirname(self.checkpoint_path) - await asyncio.to_thread(os.makedirs, checkpoint_dir, exist_ok=True) - - checkpoint_file = await download_checkpoint_from_neuron( - bucket_info=bucket_info, - neuron_hotkey=highest_stake_hotkey, - checkpoint_dir=checkpoint_dir, - ) - - if checkpoint_file: - global_step, _ = await load_checkpoint( - filename=checkpoint_file, - model=self.model, - device=self.device, - optimizer=optimizer, - scheduler=scheduler, - is_validator=is_validator, - hparams=hparams, - ) - logger.info(f"Resumed from global step {global_step}") - return global_step if global_step is not None else 0 - - logger.warning( - "Failed to download neuron checkpoint. Starting from scratch." - ) - return 0 - - logger.warning( - f"No bucket info for neuron {highest_stake_hotkey}. Starting from scratch." - ) - return 0 - - logger.warning("No neurons found. Starting from scratch.") - return 0 - - except Exception as e: - logger.error(f"Error loading checkpoint: {e}") - return 0 - - async def cleanup_old_version_checkpoints(self, keep_latest: bool = True) -> None: - """ - Cleans up checkpoint files that don't match the current version number. - Handles non-existent directories and empty paths gracefully. - - Args: - keep_latest (bool): If True, keeps the latest checkpoint from old versions - as a backup. Defaults to True. - """ - try: - checkpoint_dir = os.path.dirname(self.checkpoint_path) - - # Check if directory exists - if not os.path.exists(checkpoint_dir): - logger.debug(f"Checkpoint directory does not exist: {checkpoint_dir}") - return - - pattern = os.path.join( - checkpoint_dir, - f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b*_v*.pth", - ) - - # Get list of checkpoint files - checkpoint_files = await asyncio.to_thread(glob.glob, pattern) - if not checkpoint_files: - logger.debug(f"No checkpoint files found in {checkpoint_dir}") - return - - # Group checkpoints by version - version_groups = {} - for filepath in checkpoint_files: - if not os.path.exists(filepath): # Check if file still exists - continue - - filename = os.path.basename(filepath) - match = re.match( - rf"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b(\d+)_v(.+)\.pth", - filename, - ) - if match: - block_number = int(match.group(1)) - version = match.group(2) - if version not in version_groups: - version_groups[version] = [] - version_groups[version].append((block_number, filepath)) - - if not version_groups: - logger.debug("No valid checkpoint files found") - return - - # Identify files to delete - to_delete = [] - for version, checkpoints in version_groups.items(): - if version != __version__: # If not current version - if keep_latest: - # Sort by block number and keep only the latest - checkpoints.sort(key=lambda x: x[0], reverse=True) - to_delete.extend(filepath for _, filepath in checkpoints[1:]) - else: - # Delete all checkpoints of old versions - to_delete.extend(filepath for _, filepath in checkpoints) - - if not to_delete: - logger.debug("No old version checkpoints to clean up") - return - - # Delete files - deleted_count = 0 - for filepath in to_delete: - try: - if os.path.exists( - filepath - ): # Double check file exists before deletion - await asyncio.to_thread(os.remove, filepath) - deleted_count += 1 - logger.info(f"Deleted old version checkpoint: {filepath}") - except Exception as e: - logger.warning(f"Failed to delete checkpoint {filepath}: {e}") - - if deleted_count > 0: - logger.info(f"Cleaned up {deleted_count} old version checkpoint(s)") - - except Exception as e: - logger.error(f"Error during checkpoint cleanup: {e}") - - async def save_and_upload(self, global_step: int, block_number: int, **kwargs): - """Save and upload checkpoint asynchronously.""" - try: - start_time = asyncio.get_event_loop().time() - # Save checkpoint - await self._save_checkpoint_async(global_step, block_number, **kwargs) - save_time = asyncio.get_event_loop().time() - start_time - logger.info(f"Checkpoint save took {save_time:.2f} seconds") - - # Schedule new upload and cleanup without canceling existing ones - self.upload_task = asyncio.create_task(self._upload_and_cleanup()) - except Exception as e: - logger.error(f"Error in save_and_upload: {e}") - - async def _upload_and_cleanup(self): - """Uploads the checkpoint and cleans up old ones.""" - try: - start_time = asyncio.get_event_loop().time() - await self._upload_checkpoint_async() - upload_time = asyncio.get_event_loop().time() - start_time - logger.info(f"Checkpoint upload took {upload_time:.2f} seconds") - - cleanup_start = asyncio.get_event_loop().time() - await self._cleanup_old_checkpoints_async() - cleanup_time = asyncio.get_event_loop().time() - cleanup_start - logger.info(f"Checkpoint cleanup took {cleanup_time:.2f} seconds") - except Exception as e: - logger.exception(f"Exception in _upload_and_cleanup: {e}") - - def cleanup(self): - """Cleanup resources if needed.""" - self._shutdown = True - # Let any pending upload tasks complete - logger.info("CheckpointManager shutdown complete") - - -async def load_model_for_eval( - metagraph, - buckets: List[Optional[Union[str, Bucket]]], - model: torch.nn.Module, - checkpoint_path: str, - device: str = "cuda", -) -> tuple[int, int]: # Return (global_step, block_number) - """ - Simplified checkpoint loader that only loads model state for evaluation. - Returns tuple of (global_step, block_number). - """ - try: - # Get highest stake neuron - highest_stake_hotkey = get_neuron_with_highest_stake(metagraph, buckets) - if not highest_stake_hotkey: - logger.warning("No neurons found. Starting from scratch.") - return 0, 0 - - uid = metagraph.hotkeys.index(highest_stake_hotkey) - bucket_info = buckets[uid] - - if bucket_info: - # Download checkpoint - checkpoint_dir = os.path.dirname(checkpoint_path) - await asyncio.to_thread(os.makedirs, checkpoint_dir, exist_ok=True) - - checkpoint_file = await download_checkpoint_from_neuron( - bucket_info=bucket_info, - neuron_hotkey=highest_stake_hotkey, - checkpoint_dir=checkpoint_dir, - ) - - if checkpoint_file: - # Parse block number from filename - regex_pattern = rf"neuron_checkpoint_{highest_stake_hotkey}_b(\d+)_v({re.escape(__version__)})\.pth" - match = re.match(regex_pattern, os.path.basename(checkpoint_file)) - if not match: - logger.warning( - f"Could not parse block number from checkpoint filename: {checkpoint_file}" - ) - return 0, 0 - - block_number = int(match.group(1)) - - # Load only model state - checkpoint = torch.load( - checkpoint_file, map_location=device, weights_only=True - ) - if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint: - model.load_state_dict(checkpoint["model_state_dict"]) - global_step = checkpoint.get("global_step", 0) - logger.info( - f"Loaded model state at global step {global_step} from block {block_number}" - ) - return global_step, block_number - - logger.warning("Failed to download or load checkpoint") - return 0, 0 - - logger.warning(f"No bucket info for neuron {highest_stake_hotkey}") - return 0, 0 - - except Exception as e: - logger.error(f"Error loading checkpoint: {e}") - return 0, 0 diff --git a/src/templar/commitment.py b/src/templar/commitment.py deleted file mode 100644 index cd90ab4..0000000 --- a/src/templar/commitment.py +++ /dev/null @@ -1,166 +0,0 @@ -# Global imports -import bittensor as bt -from retry import retry -from typing import Optional, Dict -from websockets.exceptions import ConnectionClosedOK, WebSocketException -import os -import toml - -# Local imports -from .logging import logger -import templar as tplr -from templar.schemas import Bucket - - -def commit(subtensor: bt.Subtensor, wallet, netuid: int) -> None: - """Commits bucket configuration data to the subtensor network. - - This method prepares and commits bucket configuration data to the subtensor network. - The data includes: - - Account ID: A string of fixed length 32 characters - - Access key ID: A string of fixed length 32 characters - - Secret access key: A string of variable length (up to 64 characters) - - The commitment process involves: - - Concatenating the account ID, access key ID, and secret access key into a single string - - Committing the concatenated data to the subtensor network using the provided netuid and wallet - - Args: - subtensor: The subtensor network interface - wallet: The wallet used to sign the commitment transaction - netuid: The network UID to commit the data to - - Raises: - Any exceptions from the subtensor network communication are propagated - """ - concatenated = ( - tplr.config.BUCKET_SECRETS["account_id"] - + tplr.config.BUCKET_SECRETS["read"]["access_key_id"] - + tplr.config.BUCKET_SECRETS["read"]["secret_access_key"] - ) - subtensor.commit(wallet, netuid, concatenated) - logger.info(f"Committed data to the network: {concatenated}") - - -def get_all_commitments( - netuid: int, - metagraph, - config, - block: Optional[int] = None, -) -> Dict[int, Bucket]: - """Retrieves and parses all commitment data from the network for a given netuid.""" - - @retry( - exceptions=(ConnectionClosedOK, WebSocketException, Exception), - delay=2, - tries=5, - backoff=2, - max_delay=8, - ) - def query_commitments(): - # Create a fresh subtensor using the same config as the original - fresh_subtensor = bt.subtensor(config=config) - - try: - block_hash = ( - None if block is None else fresh_subtensor.get_block_hash(block) - ) - logger.info( - f"Querying commitments for netuid {netuid} at block {'latest' if block_hash is None else block_hash}" - ) - - result = fresh_subtensor.substrate.query_map( - module="Commitments", - storage_function="CommitmentOf", - params=[netuid], - block_hash=block_hash, - ) - return list(result) - finally: - # Always clean up - if ( - hasattr(fresh_subtensor.substrate, "websocket") - and fresh_subtensor.substrate.websocket - ): - fresh_subtensor.substrate.close() - - try: - result = query_commitments() - hotkey_to_uid = dict(zip(metagraph.hotkeys, metagraph.uids)) - commitments = {} - - for key, value in result: - hotkey = key.value - - # Skip blacklisted hotkeys - if hotkey in BLACKLISTED_HOTKEYS: - logger.info(f"Skipping blacklisted hotkey: {hotkey}") - continue - - if hotkey not in hotkey_to_uid: - continue - - uid = hotkey_to_uid[hotkey] - commitment_info = value.value.get("info", {}) - fields = commitment_info.get("fields", []) - - if not fields or not isinstance(fields[0], dict): - continue - - field_value = next(iter(fields[0].values())) - if field_value.startswith("0x"): - field_value = field_value[2:] - - try: - concatenated = bytes.fromhex(field_value).decode("utf-8").strip() - - if len(concatenated) != 128: - logger.debug( - f"Commitment '{concatenated}' has length {len(concatenated)}, expected 128." - ) - continue - - bucket = Bucket( - name=concatenated[:32], - account_id=concatenated[:32], - access_key_id=concatenated[32:64], - secret_access_key=concatenated[64:], - ) - - commitments[uid] = bucket - logger.debug(f"Bucket fetched and parsed for UID {uid}: {bucket.name}") - - except Exception as e: - logger.error( - f"Failed to decode and parse commitment for UID {uid}: {e}" - ) - continue - - return commitments - - except Exception as e: - logger.error(f"Failed to query commitments: {e}") - raise - logger.error(f"Failed to query commitments: {e}") - raise - - -def load_blacklisted_hotkeys(): - blacklist_file = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "..", "..", "blacklist.toml" - ) - blacklist_file = os.path.normpath(blacklist_file) - if os.path.exists(blacklist_file): - try: - config = toml.load(blacklist_file) - hotkeys = config.get("blacklist", {}).get("hotkeys", []) - return set(hotkeys) - except Exception as e: - logger.error(f"Error loading blacklist.toml: {e}") - return set() - else: - logger.warning("blacklist.toml not found.") - return set() - - -BLACKLISTED_HOTKEYS = load_blacklisted_hotkeys() diff --git a/src/templar/comms.py b/src/templar/comms.py deleted file mode 100644 index d0379b0..0000000 --- a/src/templar/comms.py +++ /dev/null @@ -1,1102 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Global imports -import aiofiles -import asyncio -import hashlib -import numpy as np -import os -import re -import tempfile -import torch -import uvloop -from aiobotocore.session import get_session -import bittensor as bt -from collections import defaultdict -from filelock import FileLock, Timeout -from types import SimpleNamespace -from typing import List, Dict, Tuple - -# Local imports -from . import __version__ -from .config import ( - BUCKET_SECRETS, - client_config, -) -from templar.constants import CF_REGION_NAME -from templar.logging import logger -from templar.schemas import Bucket - - -def get_base_url(account_id: str) -> str: - """Gets the base URL for Cloudflare R2 storage. - - Args: - account_id (str): The Cloudflare account ID - - Returns: - str: The base URL for R2 storage in the format https://{account_id}.r2.cloudflarestorage.com - """ - return f"https://{account_id}.r2.cloudflarestorage.com" - - -def get_bucket(bucket_secrets: dict[str, str | dict[str, str]]) -> Bucket: - """Creates a Bucket object from bucket secrets configuration. - - Args: - bucket_secrets (dict[str, str | dict[str, str]]): Dictionary containing bucket configuration with: - - bucket_name: Name of the bucket - - account_id: Cloudflare account ID - - read: Dict containing read access credentials: - - access_key_id: Access key ID for read operations - - secret_access_key: Secret access key for read operations - - Returns: - Bucket: A Bucket object initialized with the provided configuration - - Example: - >>> secrets = { - ... "bucket_name": "my-bucket", - ... "account_id": "abc123", - ... "read": { - ... "access_key_id": "KEY123", - ... "secret_access_key": "SECRET123" - ... } - ... } - >>> bucket = get_bucket(secrets) - """ - return Bucket( - name=bucket_secrets["bucket_name"], - account_id=bucket_secrets["account_id"], - access_key_id=bucket_secrets["read"]["access_key_id"], - secret_access_key=bucket_secrets["read"]["secret_access_key"], - ) - - -# Set uvloop as the event loop policy -asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - -# Define a semaphore to limit concurrent downloads (adjust as needed) -semaphore = asyncio.Semaphore(1000) - - -async def get_slices(filename: str, device: str) -> Dict[str, torch.Tensor]: - """ - Loads model parameter slices from a file with thread-safe locking. - Handles missing files gracefully. - """ - lock_path = f"{filename}.lock" - try: - # Check if file exists before trying to acquire lock - if not os.path.exists(filename): - logger.warning(f"Slice file not found: {filename}") - return {} - - lock = FileLock(lock_path) - with lock.acquire(timeout=1): - # Check again if file exists after acquiring lock - if not os.path.exists(filename): - logger.warning(f"Slice file not found after acquiring lock: {filename}") - return {} - try: - return torch.load( - filename, - map_location=torch.device(device), - weights_only=True, - ) - except ( - torch.serialization.pickle.UnpicklingError, - RuntimeError, - EOFError, - FileNotFoundError, - ) as e: - logger.warning(f"Failed to load slice file {filename}: {e}") - return {} - except Exception as e: - logger.warning(f"Error loading slice file {filename}: {e}") - return {} - except Timeout: - logger.warning(f"Timeout acquiring lock for {filename}") - return {} - except Exception as e: - logger.warning(f"Error during slice loading for {filename}: {e}") - return {} - finally: - # Cleanup lock file if it exists - try: - if os.path.exists(lock_path): - os.remove(lock_path) - except Exception as e: - logger.warning(f"Failed to remove lock file {lock_path}: {e}") - - -async def apply_slices_to_model( - model: torch.nn.Module, - window: int, - seed: str, - compression: int, - save_location: str, - key: str = "slice", -) -> Tuple[int, Dict[str, any]]: - """ - Applies downloaded model parameter slices to a model for a specific window, - weighting each contribution equally based on the norm of each miner's update - and preserving the overall parameter scale. - - Args: - model (torch.nn.Module): The PyTorch model to apply slices to - window (int): The window number to load slices for - seed (str): Seed used to determine which parameters to select - compression (int): Compression factor for parameter selection - save_location (str): Directory where slices are saved - key (str, optional): Prefix for the slice files. Defaults to 'slice'. - - Returns: - int: The maximum global step seen across all applied slices. - Dict: The metric dictionary aggregated participants' slices in a window - """ - max_global_step = 0 - window_metric = {} - - indices_dict = await get_indices_for_window(model, seed, compression) - slice_files = await load_files_for_window( - window=window, save_location=save_location, key=key - ) - - param_sums = { - name: torch.zeros( - len(indices_dict[name]), dtype=param.data.dtype, device=model.device - ) - for name, param in model.named_parameters() - if name in indices_dict - } - slice_norms = [] # Collect norms for computing median - num_files = 0 # Track the number of valid files - - for file_i in slice_files: - try: - filename = os.path.basename(file_i) - match = re.match( - rf"^{key}-{window}-(.+)-v{re.escape(__version__)}\.pt$", - filename, - ) - if not match: - logger.warning( - f"Skipping file {file_i} due to version mismatch in filename." - ) - continue - participant_hotkey = match.group(1) - - slice_i = await get_slices(file_i, model.device) - - # Handle both dictionary and tensor returns - if isinstance(slice_i, dict): - slice_global_step = slice_i.get("global_step") - slice_metric = slice_i.get("slice_metric") - # Remove non-tensor items from the dictionary - tensor_items = { - k: v for k, v in slice_i.items() if isinstance(v, torch.Tensor) - } - else: - # If it's not a dict, assume it's a tensor or tensor-like object - slice_global_step = None - slice_metric = None - tensor_items = slice_i - - if slice_global_step is not None: - max_global_step = max(max_global_step, slice_global_step) - - if slice_metric is not None: - window_metric[participant_hotkey] = slice_metric - - # Compute norm of the slice - slice_norm = 0.0 - slice_values = {} - - try: - for name, param in model.named_parameters(): - # Check if name exists in both dictionaries using dict methods - if ( - not isinstance(tensor_items, dict) - or name not in indices_dict.keys() - or name not in tensor_items.keys() - ): - continue - values = tensor_items[name].to(model.device) - slice_norm += torch.norm(values, p=2).item() ** 2 - slice_values[name] = values - - slice_norm = np.sqrt(slice_norm) + 1e-8 - slice_norms.append(slice_norm) - num_files += 1 - - # Normalize and accumulate - for name, values in slice_values.items(): - normalized_values = values / slice_norm - param_sums[name] += normalized_values - - except RuntimeError as e: - logger.warning(f"Skipping problematic slice {filename}: {str(e)}") - continue - - except Exception as e: - logger.error(f"Error applying slice from {file_i}: {str(e)}") - continue - - if not num_files or not slice_norms: - logger.warning(f"No valid slices found for window {window}") - return max_global_step, window_metric - - # Apply the average of normalized slices - median_norm = torch.median(torch.tensor(slice_norms)) - for name, param in model.named_parameters(): - if name not in indices_dict: - continue - param_indices = indices_dict[name].to(model.device) - avg_param = param_sums[name] / num_files - - avg_param = avg_param * median_norm - # Convert to the appropriate data type - avg_param = avg_param.to(param.data.dtype) - - # Apply the averaged and scaled parameter to the model - param.data.view(-1)[param_indices] = avg_param.clone() - - return max_global_step, window_metric - - -async def upload_slice_for_window( - bucket: str, - model: torch.nn.Module, - window: int, - seed: str, - wallet: "bt.wallet", - compression: int, - save_location: str, - key: str = "slice", - global_step: int = 0, - slice_metric: Dict[str, any] = None, -): - """ - Uploads a slice of model parameters to S3 for a specific window. - Handles concurrent file operations gracefully. - """ - filename = f"{key}-{window}-{wallet.hotkey.ss58_address}-v{__version__}.pt" - logger.debug(f"Uploading slice to S3: {filename}") - - # Prepare the slice data - indices = await get_indices_for_window(model, seed, compression) - - # Create the slice dictionary with global_step - slice_data = {"global_step": global_step} - if slice_metric is not None: - slice_data["slice_metric"] = slice_metric - # Create the slice dictionary with global_step - for name, param in model.named_parameters(): - slice_data[name] = param.data.view(-1)[indices[name].to(model.device)].cpu() - - # Use save_location for temporary file - temp_file_name = os.path.join(save_location, filename) - - try: - # Save the file - torch.save(slice_data, temp_file_name) - - # Upload the file to S3 - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - try: - with open(temp_file_name, "rb") as f: - await s3_client.put_object(Bucket=bucket, Key=filename, Body=f) - logger.debug(f"Successfully uploaded slice to S3: {filename}") - except Exception as e: - logger.warning(f"Failed to upload slice {filename} to S3: {str(e)}") - # Don't raise, allow process to continue - except Exception as e: - logger.warning( - f"Error during slice preparation/upload for {filename}: {str(e)}" - ) - # Don't raise, allow process to continue - finally: - # Clean up the temporary file if it exists - try: - if os.path.exists(temp_file_name): - os.remove(temp_file_name) - logger.debug(f"Temporary file {temp_file_name} removed") - except Exception as e: - logger.warning( - f"Failed to remove temporary file {temp_file_name}: {str(e)}" - ) - # Don't raise, allow process to continue - - -async def upload_master(bucket: str, model: torch.nn.Module, wallet: "bt.wallet"): - """ - Uploads the master PyTorch model to an S3 bucket. - - Args: - bucket (str): Name of the S3 bucket. - model (torch.nn.Module): The PyTorch model to be uploaded. - wallet (bt.wallet): The wallet object containing the hotkey. - """ - upload_filename = f"master-{wallet.hotkey.ss58_address}.pt" - logger.debug(f"Uploading master model to S3: {upload_filename}") - - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - try: - # Create a temporary file and write the model state dictionary to it - with tempfile.NamedTemporaryFile(delete=False) as temp_file: - torch.save(model.state_dict(), temp_file) - temp_file_name = temp_file.name - - # Upload the file to S3 - with open(temp_file_name, "rb") as f: - await s3_client.put_object(Bucket=bucket, Key=upload_filename, Body=f) - logger.debug(f"Successfully uploaded master model to S3: {upload_filename}") - except Exception: - logger.exception(f"Failed to upload master model {upload_filename} to S3") - finally: - # Clean up the temporary file - os.remove(temp_file_name) - logger.debug(f"Temporary file {temp_file_name} removed") - - -async def get_indices_for_window( - model: torch.nn.Module, seed: str, compression: int -) -> Dict[str, torch.LongTensor]: - """ - Computes the indices for the given window and compression factor. - - Args: - model (torch.nn.Module): The PyTorch model. - seed (str): The window seed identifier. - compression (int): The compression factor. - - Returns: - Dict[str, torch.LongTensor]: A dictionary mapping parameter names to index tensors. - """ - logger.debug( - f"Computing indices for window seed {seed} with compression {compression}" - ) - result = {} - # Seed the random number generator with the seed - seed = int(hashlib.md5(str(seed).encode("utf-8")).hexdigest(), 16) % (2**32) - rng = np.random.default_rng(seed) - for name, param in model.named_parameters(): - # Randomly select indices based on the compression factor - num_indices = max(1, int(param.numel() // compression)) - indices = rng.choice(param.numel(), size=num_indices, replace=False) - result[name] = torch.from_numpy(indices).long().cpu() - return result - - -async def download_file( - s3_client, bucket: str, filename: str, save_location: str -) -> str: - """ - Downloads a file from S3, using parallel downloads for large files. - - Args: - s3_client: The S3 client. - bucket (str): Name of the S3 bucket. - filename (str): The S3 object key (filename). - - Returns: - str: The path to the downloaded file in the temporary directory. - """ - async with semaphore: - temp_file = os.path.join(save_location, filename) - # Check if the file exists. - if os.path.exists(temp_file): - logger.debug(f"File {temp_file} already exists, skipping download.") - return temp_file - lock_file = f"{temp_file}.lock" - lock = FileLock(lock_file) - try: - # Try to acquire both locks with a timeout - with lock.acquire(timeout=1): - # Proceed to download the file - logger.debug(f"Downloading file {filename} to {temp_file}") - CHUNK_SIZE = 1 * 1024 * 1024 # 1 MB - - response = await s3_client.get_object(Bucket=bucket, Key=filename) - async with aiofiles.open(temp_file, "wb") as outfile: - while True: - chunk = await response["Body"].read(CHUNK_SIZE) - if not chunk: - break - await outfile.write(chunk) - - logger.debug(f"Successfully downloaded file {filename} to {temp_file}") - return temp_file - - except Timeout: - logger.error( - f"Timeout occurred while trying to acquire lock on {lock_file}" - ) - return None - except Exception as e: - logger.exception( - f"Failed to download file {filename} from bucket {bucket}: {e}" - ) - return None - finally: - # The lock is automatically released when exiting the 'with' block - pass - - -async def handle_file( - s3_client, - bucket: str, - filename: str, - hotkey: str, - window: int, - version: str, - save_location: str, -): - """ - Handles downloading a single file from S3. - - Args: - s3_client: The S3 client. - bucket (str): Name of the S3 bucket. - filename (str): The S3 object key (filename). - hotkey (str): The hotkey identifier. - window (int): The window identifier. - version (str): The version extracted from the filename. - - Returns: - SimpleNamespace: An object containing file metadata and the path to the downloaded file, - including the version. - """ - logger.debug( - f"Handling file '{filename}' for window {window} and hotkey '{hotkey}'" - ) - temp_file = await download_file(s3_client, bucket, filename, save_location) - if temp_file: - return SimpleNamespace( - bucket=bucket, - hotkey=hotkey, - filename=filename, - window=window, - temp_file=temp_file, - version=version, - ) - return None - - -async def validate_slice_data(slice_file: str, save_location: str) -> bool: - """ - Validates a slice file and moves it to appropriate directory based on validity. - - Args: - slice_file (str): Path to the slice file - save_location (str): Base directory for organizing slices - - Returns: - bool: True if slice is valid, False otherwise - """ - try: - # Load the slice data - slice_data = torch.load(slice_file, weights_only=True) - - # Basic validation checks - if not isinstance(slice_data, dict): - raise ValueError("Slice data is not a dictionary") - - # Check for required tensor data - has_tensors = False - for key, value in slice_data.items(): - if isinstance(value, torch.Tensor): - has_tensors = True - break - - if not has_tensors: - raise ValueError("No tensor data found in slice") - - return True - - except Exception as e: - # Handle invalid slice - - filename = os.path.basename(slice_file) - logger.warning(f"Invalid slice {filename}: {str(e)}") - - return False - - -async def process_bucket( - s3_client, bucket: str, windows: List[int], key: str, save_location: str -): - """ - Processes a single S3 bucket to download files for specified windows. - - Args: - s3_client: The S3 client to use for operations. - bucket (str): Name of the S3 bucket to process. - windows (List[int]): List of window IDs to download files for. - key (str): Prefix to filter files by. - save_location (str): Base directory for saving and organizing slices. - - Returns: - List[SimpleNamespace]: List of downloaded file metadata objects for valid slices. - """ - # Import the required modules - import re - from templar import __version__ # Ensure __version__ is imported - - logger.debug(f"Processing bucket '{bucket}' for windows {windows}") - files = [] - paginator = s3_client.get_paginator("list_objects_v2") - - for window in windows: - prefix = f"{key}-{window}" - logger.debug(f"Listing objects with prefix '{prefix}' in bucket '{bucket}'") - try: - async for page in paginator.paginate(Bucket=bucket, Prefix=prefix): - logger.trace( - f"Processing page for prefix '{prefix}' in bucket '{bucket}'" - ) - if "Contents" not in page: - logger.trace( - f"No contents found for prefix '{prefix}' in bucket '{bucket}'" - ) - continue - - download_tasks = [] - for obj in page.get("Contents", []): - filename = obj["Key"] - logger.trace( - f"Processing object with key '{filename}' in bucket '{bucket}'" - ) - try: - # Extract hotkey and version from the filename using non-greedy matching - match = re.match(rf"^{key}-{window}-(.+?)-v(.+)\.pt$", filename) - if not match: - logger.error( - f"Filename '{filename}' does not conform to the expected format." - ) - continue - slice_hotkey = match.group(1) - slice_version = match.group(2) - - # Compare version with the expected version - if slice_version != __version__: - logger.warning( - f"Skipping file '{filename}' due to version mismatch " - f"(expected {__version__}, got {slice_version})." - ) - continue - - logger.trace( - f"Parsed filename '{filename}' into window '{window}', " - f"hotkey '{slice_hotkey}', and version '{slice_version}'" - ) - - # Add the download task - download_tasks.append( - handle_file( - s3_client, - bucket, - filename, - slice_hotkey, - window, - slice_version, - save_location, - ) - ) - except ValueError: - logger.exception(f"Error parsing filename '{filename}'") - continue - except Exception as e: - logger.exception( - f"Unexpected error processing filename '{filename}': {e}" - ) - continue - - # Download and validate files concurrently - try: - results = await asyncio.gather( - *download_tasks, return_exceptions=True - ) - for res in results: - if isinstance(res, Exception): - logger.error(f"Download task failed: {res}") - continue - - if not res: - continue - - # Validate the downloaded slice - is_valid = await validate_slice_data( - res.temp_file, save_location - ) - if is_valid: - files.append(res) - - logger.trace( - f"Completed processing page for prefix '{prefix}' in bucket '{bucket}'" - ) - except Exception as e: - logger.exception( - f"Error during asyncio.gather for prefix '{prefix}': {e}" - ) - - except Exception as e: - logger.error( - f"Error listing objects in bucket '{bucket}' with prefix '{prefix}': {e}" - ) - - logger.trace(f"Completed processing bucket '{bucket}' for windows {windows}") - return files - - -async def download_slices_for_buckets_and_windows( - buckets: List[Bucket], windows: List[int], key: str, save_location: str -) -> Dict[int, List[SimpleNamespace]]: - """Downloads model slices from multiple S3 buckets for specified windows. - - This function downloads model slice files from a list of S3 buckets for the given window IDs. - It processes the buckets concurrently and combines the results into a dictionary mapping - window IDs to lists of downloaded slices. - - Args: - buckets (List[Bucket]): List of Bucket objects containing S3 credentials and configuration - windows (List[int]): List of window IDs to download slices for - key (str, optional): Prefix to filter files by. Defaults to "slice" - - Returns: - Dict[int, List[SimpleNamespace]]: Dictionary mapping window IDs to lists of downloaded slices. - Each slice is represented as a SimpleNamespace object containing metadata and file path. - - Example: - >>> buckets = [Bucket(...), Bucket(...)] # List of bucket configs - >>> windows = [1, 2, 3] # Window IDs to download - >>> slices = await download_slices_for_buckets_and_windows(buckets, windows) - >>> print(slices[1]) # Get all slices for window 1 - [Slice(path='/tmp/slice-1-abc.pt'), Slice(path='/tmp/slice-1-def.pt')] - - Note: - - Filters out None buckets from input list - - Downloads files concurrently across buckets - - Uses CloudFront for downloads if configured - - Handles S3 authentication using bucket credentials - - Returns empty dict if no valid buckets provided - """ - # Filter out None buckets - # Filter out None buckets - valid_buckets = [] - for b in buckets: - if b is None: - continue - if isinstance(b, str): - logger.warning(f"Received string instead of Bucket object: {b}") - continue - if not isinstance(b, Bucket): - logger.warning(f"Invalid bucket type: {type(b)}") - continue - valid_buckets.append(b) - - if not valid_buckets: - logger.warning("No valid buckets provided") - return {} - - try: - logger.debug( - f"Downloading files for buckets {[b.name for b in valid_buckets]} and windows {windows}" - ) - except Exception as e: - logger.error(f"Error logging bucket names: {e}") - return {} - - session = get_session() - tasks = [] - for bucket in set(valid_buckets): - async with session.create_client( - "s3", - endpoint_url=get_base_url(bucket.account_id), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=bucket.access_key_id, - aws_secret_access_key=bucket.secret_access_key, - ) as s3_client: - logger.debug(f"Processing bucket: {bucket.name}") - tasks.append( - process_bucket(s3_client, bucket.name, windows, key, save_location) - ) - - results = await asyncio.gather(*tasks) - # Combine results into a dictionary mapping window IDs to lists of slices - slices = defaultdict(list) - for result in results: - for item in result: - slices[item.window].append(item) - return slices - - -async def load_files_for_window( - window: int, save_location: str, key: str = "slice" -) -> List[str]: - """ - Loads files for a specific window from the temporary directory. - - Args: - window (int): The window identifier to load files for - key (str, optional): The prefix to filter files by. Defaults to 'slice'. - - Returns: - List[str]: A list of full file paths matching the window and key pattern - - Example: - >>> files = await load_files_for_window(123, 'state') - >>> print(files) - ['/tmp/state-123-abc-v1.0.0.pt', '/tmp/state-123-def-v1.0.0.pt'] - - Note: - - Only returns files matching pattern: {key}-{window}-*-v{version}.pt - - Files must be in the system temp directory - - Version number is pulled from templar.__version__ - """ - - logger.debug(f"Retrieving files for window {window} from temporary directory") - temp_dir = save_location - window_files = [] - pattern = re.compile(rf"^{key}-{window}-.+-v{__version__}\.pt$") - for filename in os.listdir(temp_dir): - if pattern.match(filename): - window_files.append(os.path.join(temp_dir, filename)) - logger.debug(f"Found file {filename} for window {window}") - return window_files - - -async def delete_files_before_window( - window_max: int, save_location: str, key: str = "slice" -): - """ - Deletes temporary files with window IDs less than the specified maximum. - - Args: - window_max (int): Maximum window ID to keep. Files with window IDs less than this will be deleted - key (str, optional): The prefix to filter files by. Defaults to 'slice' - - Example: - >>> await delete_files_before_window(100, 'state') - # Deletes all state-*.pt files with window < 100 - - Note: - - Deletes both .pt and .pt.lock files - - Only deletes files matching pattern: {key}-{window}-*-v{version}.pt - - Files must be in system temp directory - - Version number is pulled from templar.__version__ - """ - - logger.debug(f"Deleting files with window id before {window_max}") - temp_dir = save_location - pattern = re.compile(rf"^{re.escape(key)}-(\d+)-.+-v{__version__}\.(pt|pt\.lock)$") - for filename in os.listdir(temp_dir): - match = pattern.match(filename) - if match: - try: - window_id = int(match.group(1)) - if window_id < window_max: - file_path = os.path.join(temp_dir, filename) - if os.path.exists(file_path): - os.remove(file_path) - logger.debug(f"Deleted file {file_path}") - except Exception as e: - logger.error(f"Error deleting file {filename}: {e}") - - -async def delete_files_from_bucket_before_window( - bucket: str, window_max: int, key: str = "slice" -): - """ - Deletes files from an S3 bucket with window IDs less than the specified maximum. - - Args: - bucket (str): Name of the S3 bucket to delete files from - window_max (int): Maximum window ID to keep. Files with window IDs less than this will be deleted - key (str, optional): The prefix to filter files by. Defaults to 'slice' - - Example: - >>> await delete_files_from_bucket_before_window('my-bucket', 100, 'state') - # Deletes all state-*.pt files with window < 100 from my-bucket - - Note: - - Deletes both .pt and .pt.lock files - - Only deletes files matching pattern: {key}-{window}-*-v{version}.pt - - Version number is pulled from templar.__version__ - - Requires valid AWS credentials and bucket permissions - """ - - logger.debug( - f"Deleting files in bucket {bucket} with window id before {window_max}" - ) - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - try: - response = await s3_client.list_objects_v2(Bucket=bucket) - if "Contents" in response: - for obj in response["Contents"]: - filename = obj["Key"] - match = re.match( - rf"^{re.escape(key)}-(\d+)-.+-v{__version__}\.(pt|pt\.lock)$", - filename, - ) - if match: - try: - window_id = int(match.group(1)) - if window_id < window_max: - await s3_client.delete_object( - Bucket=bucket, Key=filename - ) - logger.debug( - f"Deleted file {filename} from bucket {bucket}" - ) - except Exception as e: - logger.error( - f"Error deleting file {filename} from bucket {bucket}: {e}" - ) - except Exception as e: - logger.error(f"Error listing objects in bucket {bucket}: {e}") - - -BUCKET_REGEX = re.compile( - r"^(?=.{3,63}$)(?!.*\.\.)(?!\-)(?!\.)(?!.*\.$)[a-z0-9]+(?:[\.-][a-z0-9]+)*$" -) - -ARN_REGEX = re.compile( - r"^arn:(aws|aws-cn|aws-us-gov):s3-object-lambda:[a-z0-9\-]+:\d{12}:accesspoint[/:][a-zA-Z0-9.\-_]{1,63}$" - r"|^arn:(aws|aws-cn|aws-us-gov):s3-outposts:[a-z0-9\-]+:\d{12}:outpost[/:][a-zA-Z0-9.\-_]{1,63}[/:]accesspoint[/:][a-zA-Z0-9\-]{1,63}$" -) - - -async def delete_old_version_files(bucket_name: str, current_version: str): - """ - Deletes files from the S3 bucket that do not match the current version. - - Args: - bucket_name (str): The name of the S3 bucket. - current_version (str): The current version string. - """ - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - paginator = s3_client.get_paginator("list_objects_v2") - async for page in paginator.paginate(Bucket=bucket_name): - to_delete = [] - for obj in page.get("Contents", []): - filename = obj["Key"] - # Check if the file version matches the current version - match = re.match(r".+-v(.+)\.pt$", filename) - if match: - file_version = match.group(1) - if file_version != current_version: - to_delete.append({"Key": filename}) - logger.debug(f"Scheduled for deletion: {filename}") - # Delete old versions in batches of 1000 (S3 limit for delete_objects) - if to_delete: - response = await s3_client.delete_objects( - Bucket=bucket_name, Delete={"Objects": to_delete} - ) - deleted = response.get("Deleted", []) - logger.info( - f"Deleted {len(deleted)} old version files from bucket {bucket_name}" - ) - - -# def is_valid_bucket(bucket_name: str) -> bool: -# """ -# Validates if the bucket name matches AWS S3 bucket naming rules -# and checks if the bucket exists and is accessible. - -# Args: -# bucket_name (str): The bucket name to validate. - -# Returns: -# bool: True if valid and accessible, False otherwise. -# """ -# # Ensure bucket_name is a string -# if isinstance(bucket_name, bytes): -# bucket_name = bucket_name.decode('utf-8') - -# # # Check if the bucket name matches the regex -# # if not (BUCKET_REGEX.match(bucket_name) or ARN_REGEX.match(bucket_name)): -# # logger.debug(f"Invalid bucket name format: {bucket_name}") -# # return False - -# # Create S3 client -# s3_client = boto3.client( -# 's3', -# region_name=CF_REGION_NAME, -# aws_access_key_id=AWS_ACCESS_KEY_ID, -# aws_secret_access_key=AWS_SECRET_ACCESS_KEY, -# config=client_config -# ) - -# # Check if the bucket exists and is accessible -# try: -# # Try to list objects in the bucket -# s3_client.list_objects_v2(Bucket=bucket_name, MaxKeys=1) -# logger.debug(f"Bucket '{bucket_name}' exists and is accessible.") -# return True # Bucket exists and is accessible -# except ClientError as e: -# error_code = e.response['Error']['Code'] -# if error_code in ['NoSuchBucket', '404']: -# logger.debug(f"Bucket '{bucket_name}' does not exist.") -# elif error_code in ['AccessDenied', '403']: -# logger.debug(f"Access denied for bucket '{bucket_name}'.") -# elif error_code == 'AllAccessDisabled': -# logger.debug(f"All access disabled for bucket '{bucket_name}'.") -# else: -# logger.debug(f"Error accessing bucket '{bucket_name}': {e}") -# return False -# except Exception as e: -# logger.debug(f"Unexpected error when accessing bucket '{bucket_name}': {e}") -# return False - -# def validate_bucket_or_exit(bucket_name: str): -# """ -# Validates the bucket name and exits the program if invalid. - -# Args: -# bucket_name (str): The name of the S3 bucket. -# """ -# logger.debug("Validating Bucket name") -# if not is_valid_bucket(bucket_name): -# logger.error(f"Bucket name {bucket_name} is invalid. Please refer to the AWS documentation on naming conventions ") -# sys.exit(1) - - -async def save_checkpoint( - filename, model, optimizer=None, scheduler=None, global_step=0, **kwargs -): - """ - Saves the checkpoint to the specified filename asynchronously. - - Args: - filename (str): Path to save the checkpoint. - model (torch.nn.Module): The model to save. - optimizer (torch.optim.Optimizer, optional): Optimizer to save. - scheduler (torch.optim.lr_scheduler._LRScheduler, optional): Scheduler to save. - global_step (int): The current global step. - **kwargs: Additional state variables to save. - """ - # Gather the checkpoint data - checkpoint = { - "global_step": global_step, - "model_state_dict": model.state_dict(), - } - if optimizer: - checkpoint["optimizer_state_dict"] = optimizer.state_dict() - if scheduler: - checkpoint["scheduler_state_dict"] = scheduler.state_dict() - # Include additional state variables - for key, value in kwargs.items(): - checkpoint[key] = value - - # Save the checkpoint asynchronously to avoid blocking the main thread - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, torch.save, checkpoint, filename) - torch.save(checkpoint, filename) - - -async def load_checkpoint( - filename, model, optimizer=None, scheduler=None, device="cpu" -): - """ - Loads the checkpoint from the specified filename. - - Args: - filename (str): Path to the checkpoint file. - model (torch.nn.Module): The model to load the state into. - optimizer (torch.optim.Optimizer, optional): Optimizer to load the state into. - scheduler (torch.optim.lr_scheduler._LRScheduler, optional): Scheduler to load the state into. - device (str): Device to map the checkpoint. - - Returns: - global_step (int): The global step at which the checkpoint was saved. - additional_state (dict): Dictionary of additional state variables. - """ - try: - checkpoint = torch.load(filename, map_location=device, weights_only=True) - model.load_state_dict(checkpoint["model_state_dict"]) - if optimizer and "optimizer_state_dict" in checkpoint: - optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) - if scheduler and "scheduler_state_dict" in checkpoint: - scheduler.load_state_dict(checkpoint["scheduler_state_dict"]) - global_step = checkpoint.get("global_step", 0) - additional_state = { - k: checkpoint[k] - for k in checkpoint - if k - not in [ - "global_step", - "model_state_dict", - "optimizer_state_dict", - "scheduler_state_dict", - ] - } - return global_step, additional_state - except (torch.serialization.pickle.UnpicklingError, RuntimeError, EOFError) as e: - logger.error(f"Checkpoint at {filename} is corrupt: {e}") - # Return global_step as 0 and an empty additional_state - return 0, {} - except Exception as e: - logger.error(f"Failed to load checkpoint from {filename}: {e}") - return 0, {} - - -def get_neuron_temp_dir(wallet) -> str: - """ - Returns a unique temporary directory for the neuron based on its wallet hotkey. - """ - - temp_dir = os.path.join( - tempfile.gettempdir(), f"neuron_{wallet.hotkey.ss58_address}" - ) - os.makedirs(temp_dir, exist_ok=True) - return temp_dir diff --git a/src/templar/config.py b/src/templar/config.py deleted file mode 100644 index 6153c2e..0000000 --- a/src/templar/config.py +++ /dev/null @@ -1,48 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Global imports -import os -import sys -import yaml -from pathlib import Path - -# Local imports -import botocore.config -from dotenv import dotenv_values -from loguru import logger - -# Load environment variables -env_config = {**dotenv_values(".env"), **os.environ} -AWS_ACCESS_KEY_ID = env_config.get("AWS_ACCESS_KEY_ID") -AWS_SECRET_ACCESS_KEY = env_config.get("AWS_SECRET_ACCESS_KEY") - -envfile_path = Path(__file__).parents[2] / ".env.yaml" -try: - with open(envfile_path, "r") as file: - BUCKET_SECRETS = yaml.safe_load(file) -except FileNotFoundError: - logger.error( - f"{envfile_path} not found. Please create it with the help of `.env-template.yaml`." - ) - sys.exit() -BUCKET_SECRETS["bucket_name"] = BUCKET_SECRETS["account_id"] - -# Configure the S3 client -client_config = botocore.config.Config( - max_pool_connections=256, -) diff --git a/src/templar/constants.py b/src/templar/constants.py deleted file mode 100644 index 5a7250e..0000000 --- a/src/templar/constants.py +++ /dev/null @@ -1 +0,0 @@ -CF_REGION_NAME: str = "enam" diff --git a/src/templar/hparams.py b/src/templar/hparams.py deleted file mode 100644 index babcc17..0000000 --- a/src/templar/hparams.py +++ /dev/null @@ -1,74 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Global imports -import json -from types import SimpleNamespace - -# Local imports -from transformers import AutoTokenizer, LlamaConfig - -# Cache file path -HPARAMS_FILE = "hparams.json" - - -def create_namespace(hparams: dict) -> SimpleNamespace: - """ - Create a SimpleNamespace from the hyperparameters and add model configuration. - - Args: - hparams (dict): Hyperparameters dictionary. - - Returns: - SimpleNamespace: Namespace containing hyperparameters and model configuration. - """ - hparams_ns = SimpleNamespace(**hparams) - - hparams_ns.tokenizer = AutoTokenizer.from_pretrained( - hparams_ns.tokenizer_name, verbose=False, clean_up_tokenization_spaces=True - ) - hparams_ns.tokenizer.pad_token = hparams_ns.tokenizer.eos_token - - hparams_ns.model_config = LlamaConfig( - vocab_size=hparams_ns.tokenizer.vocab_size, - hidden_size=hparams_ns.hidden_size, - num_hidden_layers=hparams_ns.num_hidden_layers, - num_attention_heads=hparams_ns.num_attention_heads, - intermediate_size=hparams_ns.intermediate_size, - num_key_value_heads=hparams_ns.num_key_value_heads, - activation_function=hparams_ns.activation_function, - max_position_embeddings=hparams_ns.max_position_embeddings, - ) - - return hparams_ns - - -def load_hparams() -> SimpleNamespace: - """ - Load hyperparameters from local hparams.json file. - - Returns: - SimpleNamespace: A namespace containing the hyperparameters and model configuration. - - Example: - hparams = load_hparams() - print(hparams.hidden_size) - print(hparams.model_config) - """ - with open(HPARAMS_FILE, "r") as f: - hparams = json.load(f) - return create_namespace(hparams) diff --git a/src/templar/learning_rates.py b/src/templar/learning_rates.py deleted file mode 100644 index c383868..0000000 --- a/src/templar/learning_rates.py +++ /dev/null @@ -1,84 +0,0 @@ -# Global imports -import math -from functools import partial -from torch.optim.lr_scheduler import LambdaLR - - -def _get_wsd_lr_lambda( - current_step: int, - *, - num_warmup_steps: int, - num_stable_steps: int, - num_decay_steps: int, -) -> float: - """ - Calculates the learning rate multiplier for Warmup-Stable-Decay (WSD) schedule. - - The schedule consists of three phases: - 1. Warmup: Linear increase from 0 to 1 over num_warmup_steps - 2. Stable: Constant learning rate of 1.0 for num_stable_steps - 3. Decay: Square root decay from 1.0 to 0.0 over num_decay_steps - - Args: - current_step (int): Current training step - num_warmup_steps (int): Number of warmup steps - num_stable_steps (int): Number of steps at constant learning rate - num_decay_steps (int): Number of decay steps - - Returns: - float: Learning rate multiplier between 0.0 and 1.0 - """ - if current_step < num_warmup_steps: - # Warmup phase: increase linearly from 0 to 1 - return float(current_step) / float(max(1, num_warmup_steps)) - elif current_step < num_warmup_steps + num_stable_steps: - # Stable phase: keep learning rate constant at 1.0 - return 1.0 - else: - # Decay phase: decrease following a 1 - sqrt(x) schedule - decay_step = current_step - num_warmup_steps - num_stable_steps - decay_progress = float(decay_step) / float(max(1, num_decay_steps)) - return max(0.0, 1 - math.sqrt(decay_progress)) - - -def get_wsd_scheduler( - optimizer, - num_warmup_steps: int, - num_stable_steps: int, - num_decay_steps: int, - last_epoch: int = -1, -) -> LambdaLR: - """ - Creates a learning rate scheduler with Warmup-Stable-Decay schedule. - - This scheduler adjusts the learning rate according to three phases: - 1. Linear warmup for num_warmup_steps - 2. Constant learning rate for num_stable_steps - 3. Square root decay for num_decay_steps - - Args: - optimizer: PyTorch optimizer - num_warmup_steps (int): Number of warmup steps - num_stable_steps (int): Number of steps at constant learning rate - num_decay_steps (int): Number of decay steps - last_epoch (int, optional): The index of last epoch. Default: -1 - - Returns: - LambdaLR: PyTorch learning rate scheduler - - Example: - >>> optimizer = AdamW(model.parameters(), lr=1e-3) - >>> scheduler = get_wsd_scheduler( - ... optimizer, - ... num_warmup_steps=1000, - ... num_stable_steps=10000, - ... num_decay_steps=5000 - ... ) - """ - lr_lambda = partial( - _get_wsd_lr_lambda, - num_warmup_steps=num_warmup_steps, - num_stable_steps=num_stable_steps, - num_decay_steps=num_decay_steps, - ) - return LambdaLR(optimizer, lr_lambda, last_epoch) diff --git a/src/templar/schemas.py b/src/templar/schemas.py deleted file mode 100644 index 406d22f..0000000 --- a/src/templar/schemas.py +++ /dev/null @@ -1,24 +0,0 @@ -from pydantic import BaseModel, ConfigDict - - -class Bucket(BaseModel): - """Configuration for a bucket, including name and access credentials.""" - - model_config = ConfigDict(str_min_length=1, str_strip_whitespace=True) - - def __hash__(self): - # Use all fields to generate a unique hash - return hash( - (self.name, self.account_id, self.access_key_id, self.secret_access_key) - ) - - def __eq__(self, other): - # Compare all fields to determine equality - if isinstance(other, Bucket): - return self.dict() == other.dict() - return False - - name: str - account_id: str - access_key_id: str - secret_access_key: str diff --git a/src/templar/wandb.py b/src/templar/wandb.py deleted file mode 100644 index 095ac76..0000000 --- a/src/templar/wandb.py +++ /dev/null @@ -1,89 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -# fmt: off - - -# Global imports -import os -import wandb - -# Local imports -from templar import __version__, logger - -def initialize_wandb(run_prefix, uid, config, group, job_type): - # Ensure the wandb directory exists - wandb_dir = os.path.join(os.getcwd(), 'wandb') - os.makedirs(wandb_dir, exist_ok=True) - - # Define the run ID file path inside the wandb directory - run_id_file = os.path.join( - wandb_dir, f"wandb_run_id_{run_prefix}{uid}_{__version__}.txt" - ) - - # Check for existing run and verify it still exists in wandb - run_id = None - if os.path.exists(run_id_file): - with open(run_id_file, 'r') as f: - run_id = f.read().strip() - - # Verify if run still exists in wandb - try: - api = wandb.Api() - api.run(f"tplr/{config.project}-v{__version__}/{run_id}") - logger.info(f"Found existing run ID: {run_id}") - except Exception: - # Run doesn't exist anymore, clear the run_id - logger.info(f"Previous run {run_id} not found in WandB, starting new run") - run_id = None - os.remove(run_id_file) - - # Initialize WandB - run = wandb.init( - project=f"{config.project}-v{__version__}", - entity='tplr', - id=run_id, - resume='must' if run_id else 'never', - name=f'{run_prefix}{uid}', - config=config, - group=group, - job_type=job_type, - dir=wandb_dir, - settings=wandb.Settings( - init_timeout=300, - _disable_stats=True, - ) - ) - - # Special handling for evaluator - if run_prefix == "E": - tasks = config.tasks.split(',') - for task in tasks: - metric_name = f"eval/{task}" - # Set up x/y plot configuration - wandb.define_metric( - name=metric_name, - step_metric="global_step", # This sets global_step as x-axis - plot=True, # Ensure it creates a line plot - summary="max" - ) - - # Save run ID for future resumption - if not run_id: - with open(run_id_file, 'w') as f: - f.write(run.id) - - return run diff --git a/src/templar/__init__.py b/src/tplr/__init__.py similarity index 70% rename from src/templar/__init__.py rename to src/tplr/__init__.py index 898230d..ec3cb93 100644 --- a/src/templar/__init__.py +++ b/src/tplr/__init__.py @@ -2,14 +2,14 @@ # © 2024 templar.tech # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER @@ -20,19 +20,14 @@ # mypy: ignore-errors # type: ignore -__version__ = "0.1.33" -version_key = 4000 +__version__ = "0.2.0" # Import package. -from .autoupdate import * from .chain import * -from .checkpoint import * -from .commitment import * from .comms import * -from .config import * -from .commitment import * +from .compress import * from .dataset import * from .hparams import * -from .learning_rates import * from .logging import * -from .wandb import * +from .schemas import * +from .wandb import initialize_wandb diff --git a/src/tplr/chain.py b/src/tplr/chain.py new file mode 100644 index 0000000..7aaef82 --- /dev/null +++ b/src/tplr/chain.py @@ -0,0 +1,455 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import os +import time +import yaml +import torch +import asyncio +import numpy as np +import bittensor as bt +from typing import Dict, Optional +from bittensor import Wallet +from pydantic import ValidationError + +# Local imports +from .logging import logger +from .schemas import Bucket + + +class ChainManager: + """Base class for handling chain interactions.""" + + def __init__( + self, + config, + netuid: Optional[int] = None, + metagraph=None, + hparams=None, + fetch_interval: int = 60, # Fetch interval in seconds + wallet: Optional["bt.wallet"] = None, + bucket: Optional[Bucket] = None, + ): + """ + Initialize chain commitment handler. + + Args: + subtensor (bt.Subtensor): Subtensor instance for chain operations + netuid (int): Network UID for chain operations + metagraph: Metagraph instance containing network state + hparams: Hyperparameters namespace containing model configuration + fetch_interval (int): Interval in seconds between fetching commitments + wallet (bt.wallet, optional): Wallet to sign commitments + bucket (Bucket, optional): Bucket configuration to commit + """ + # self.subtensor = bt.subtensor(config=config) + # chain argument instead + self.config = config + self.netuid = netuid + self.metagraph = metagraph + self.hparams = hparams or {} + + # Block and window tracking + self.current_block = 0 + self.current_window = 0 + self.window_duration = self.hparams.blocks_per_window + self.window_time = 0 + self.window_seeds = {} + + # Events + self.block_event = asyncio.Event() + self.new_window_event = asyncio.Event() + + # Initialize bucket storage + self.commitments = {} + self.peers = [] + self.fetch_interval = fetch_interval + self._fetch_task = None + + # Store wallet and bucket + self.wallet = wallet + self.bucket = bucket + + # Try to commit bucket to the chain + if self.wallet and self.bucket: + # Commit bucket synchronously + asyncio.run(self.try_commit(self.wallet, self.bucket)) + else: + logger.warning("Wallet and bucket not provided; skipping try_commit.") + + # Fetch commitments synchronously to populate self.commitments + self.fetch_commitments() + + # Start fetching commitments + self.start_commitment_fetcher() + + def start_commitment_fetcher(self): + """Starts the background task to fetch commitments periodically.""" + if self._fetch_task is None: + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + self._fetch_task = loop.create_task(self._fetch_commitments_periodically()) + + async def _fetch_commitments_periodically(self): + """Background task to periodically fetch commitments.""" + while True: + try: + commitments = await self.get_commitments() + if commitments: + self.commitments = commitments + self.update_peers_with_buckets() + logger.debug(f"Updated commitments: {self.commitments}") + except Exception as e: + logger.error(f"Error fetching commitments: {e}") + await asyncio.sleep(self.fetch_interval) + + def get_bucket(self, uid: int) -> Optional[Bucket]: + """Helper function to get the bucket for a given UID. + + Args: + uid (int): The UID to retrieve the bucket for. + + Returns: + Optional[Bucket]: The bucket corresponding to the UID, or None if not found. + """ + return self.commitments.get(uid) + + def get_all_buckets(self) -> Dict[int, Optional[Bucket]]: + """Helper function to get all buckets for all UIDs in the metagraph. + + Returns: + Dict[int, Optional[Bucket]]: Mapping of UIDs to their bucket configurations + """ + return {uid: self.get_bucket(uid) for uid in self.metagraph.uids} + + def block_to_window(self, block: int) -> int: + """Returns the slice window based on a block.""" + return int(block / self.hparams.window_length) + + def window_to_seed(self, window: int) -> str: + """Returns the slice window based on a block.""" + return str(self.subtensor.get_block_hash(window * self.hparams.window_length)) + + def block_listener(self, loop): + """Listens for new blocks and updates current block/window state. + + Args: + loop: The event loop to run the listener in + + This method subscribes to block headers from the subtensor network and: + - Updates self.current_block with the latest block number + - Updates self.current_window when crossing window boundaries + - Retries on connection errors until stop_event is set + """ + + def handler(event, _u, _s): + self.current_block = int(event["header"]["number"]) + if ( + int(self.current_block / self.hparams.blocks_per_window) + != self.current_window + ): + self.current_window = int( + self.current_block / self.hparams.blocks_per_window + ) + + while not self.stop_event.is_set(): + try: + bt.subtensor(config=self.config).substrate.subscribe_block_headers( + handler + ) + break + except Exception: + time.sleep(1) + + async def commit(self, wallet: "bt.wallet", bucket: Bucket) -> None: + """Commits bucket configuration to the chain. + + Args: + wallet (bt.wallet): Wallet to sign the commitment + bucket (Bucket): Bucket configuration to commit + """ + subtensor = bt.subtensor(config=self.config) + concatenated = ( + bucket.account_id + bucket.access_key_id + bucket.secret_access_key + ) + subtensor.commit(wallet, self.netuid, concatenated) + logger.info( + f"Committed bucket configuration to chain for hotkey {wallet.hotkey.ss58_address}" + ) + + async def try_commit(self, wallet: Wallet, bucket: Bucket) -> None: + """Attempts to verify existing commitment matches current bucket config and commits if not. + + Args: + wallet (bt.wallet): Wallet to sign the commitment + bucket (Bucket): Current bucket configuration to verify/commit + """ + try: + # Get existing commitment + commitment = self.get_commitment( + self.metagraph.hotkeys.index(wallet.hotkey.ss58_address) + ) + + # Convert Bucket objects to concatenated strings for comparison + commitment_str = ( + commitment.name + + commitment.access_key_id + + commitment.secret_access_key + ) + current_str = bucket.name + bucket.access_key_id + bucket.secret_access_key + + logger.debug( + f"Comparing:\nCommitment: {commitment_str}\nCurrent: {current_str}" + ) + + if current_str != commitment_str: + raise ValueError("Bucket commitment data does not match") + + except Exception as e: + logger.error(f"Commitment error: {str(e)}") + await self.commit(wallet, bucket) + + def get_commitment(self, uid: int) -> Bucket: + """Retrieves and parses committed bucket configuration data for a given + UID. + + This method fetches commitment data for a specific UID from the + subtensor network and decodes it into a structured format. The + retrieved data is split into the following fields: + - Account ID: A string of fixed length 32 characters. + - Access key ID: A string of fixed length 32 characters. + - Secret access key: A string of variable length (up to 64 characters). + + The parsed fields are then mapped to an instance of the `Bucket` class. + When initializing the Bucket object, the account ID is also used as the + bucket name. + + The retrieval process involves: + - Fetching the commitment data for the specified UID using the + configured `netuid` from the subtensor network. + - Splitting the concatenated string into individual fields based on + their expected lengths and order. + - Mapping the parsed fields to a `Bucket` instance. + + **Note:** The order of fields (bucket name, account ID, access key ID, + secret access key) in the concatenated string is critical for accurate + parsing. + + Args: + uid: The UID of the neuron whose commitment data is being + retrieved. + + Returns: + Bucket: An instance of the `Bucket` class containing the parsed + bucket configuration details. + + Raises: + ValueError: If the parsed data does not conform to the expected + format for the `Bucket` class. + Exception: If an error occurs while retrieving the commitment data + from the subtensor network. + """ + + subtensor = bt.subtensor(config=self.config) + try: + concatenated = subtensor.get_commitment(self.netuid, uid) + logger.success(f"Commitment fetched: {concatenated}") + except Exception as e: + raise Exception(f"Couldn't get commitment from uid {uid} because {e}") + if len(concatenated) != 128: + raise ValueError( + f"Commitment '{concatenated}' is of length {len(concatenated)} but should be of length 128." + ) + + try: + return Bucket( + name=concatenated[:32], + account_id=concatenated[:32], + access_key_id=concatenated[32:64], + secret_access_key=concatenated[64:], + ) + except ValidationError as e: + raise ValueError(f"Invalid data in commitment: {e}") + + async def get_commitments(self, block: Optional[int] = None) -> Dict[int, Bucket]: + """Retrieves all bucket commitments from the chain. + + Args: + block (int, optional): Block number to query at + + Returns: + Dict[int, Bucket]: Mapping of UIDs to their bucket configurations + """ + # if self.netuid or not self.metagraph: + # raise ValueError( + # "Subtensor, netuid and metagraph must be set for chain operations" + # ) + subtensor = bt.subtensor(config=self.config) + substrate = subtensor.substrate + result = substrate.query_map( + module="Commitments", + storage_function="CommitmentOf", + params=[self.netuid], + block_hash=None if block is None else substrate.get_block_hash(block), + ) + + hotkey_to_uid = dict(zip(self.metagraph.hotkeys, self.metagraph.uids)) + commitments = {} + + for key, value in result: + hotkey = key.value + if hotkey not in hotkey_to_uid: + continue + + uid = hotkey_to_uid[hotkey] + commitment_info = value.value.get("info", {}) + fields = commitment_info.get("fields", []) + + if not fields or not isinstance(fields[0], dict): + continue + + field_value = next(iter(fields[0].values())) + if field_value.startswith("0x"): + field_value = field_value[2:] + + try: + concatenated = bytes.fromhex(field_value).decode("utf-8").strip() + if len(concatenated) != 128: + logger.error( + f"Invalid commitment length for UID {uid}: {len(concatenated)}" + ) + continue + + bucket = Bucket( + name=concatenated[:32], + account_id=concatenated[:32], + access_key_id=concatenated[32:64], + secret_access_key=concatenated[64:], + ) + commitments[uid] = bucket + logger.success(f"Retrieved bucket commitment for UID {uid}") + + except Exception as e: + logger.error(f"Failed to decode commitment for UID {uid}: {e}") + continue + + return commitments + + async def get_bucket_for_neuron(self, wallet: "bt.wallet") -> Optional[Bucket]: + """Get bucket configuration for a specific neuron's wallet + + Args: + wallet (bt.wallet): The wallet to get bucket for + + Returns: + Optional[Bucket]: The bucket assigned to this neuron, or None if not found + """ + try: + # Get UID by finding hotkey's index in metagraph + uid = self.metagraph.hotkeys.index(wallet.hotkey.ss58_address) + return await self.get_bucket(uid) + except ValueError: + logger.warning( + f"Hotkey {wallet.hotkey.ss58_address} not found in metagraph" + ) + return None + + def fetch_commitments(self): + """Synchronously fetches commitments and updates self.commitments.""" + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + commitments = loop.run_until_complete(self.get_commitments()) + if commitments: + self.commitments = commitments + self.update_peers_with_buckets() + logger.debug(f"Fetched commitments synchronously: {self.commitments}") + else: + logger.warning("No commitments fetched.") + + def get_hotkey(self, uid: int) -> Optional[str]: + """Returns the hotkey for a given UID.""" + # Handle different data types for uids + if isinstance(self.metagraph.uids, (np.ndarray, torch.Tensor)): + uids_list = self.metagraph.uids.tolist() + else: + uids_list = self.metagraph.uids + + # Handle different data types for hotkeys + if isinstance(self.metagraph.hotkeys, (np.ndarray, torch.Tensor)): + hotkeys_list = self.metagraph.hotkeys.tolist() + else: + hotkeys_list = self.metagraph.hotkeys + + if uid in uids_list: + index = uids_list.index(uid) + return hotkeys_list[index] + else: + return None + + def update_peers_with_buckets(self): + """Updates the list of peers (UIDs) that have buckets, excluding validators.""" + # Create a mapping from UIDs to their stakes + uid_to_stake = dict(zip(self.metagraph.uids.tolist(), self.metagraph.S.tolist())) + + # Filter peers that have buckets and have stake <= 10000 (miners) + self.peers = [ + int(uid) for uid in self.commitments.keys() + if uid_to_stake.get(int(uid), 0) <= 10000 + ] + logger.info(f"Updated peers with buckets (excluding validators): {self.peers}") + + +def get_own_bucket() -> Bucket: + """Parses the credentials from .env.yaml to create a Bucket object.""" + env_file = ".env.yaml" + if not os.path.isfile(env_file): + logger.error(f"The {env_file} file was not found.") + raise FileNotFoundError(f"The {env_file} file was not found.") + + try: + with open(env_file, "r") as file: + credentials = yaml.safe_load(file) + except yaml.YAMLError as e: + logger.error(f"Error parsing {env_file}: {e}") + raise e + + try: + account_id = credentials["account_id"] + read_access_key_id = credentials["read"]["access_key_id"] + read_secret_access_key = credentials["read"]["secret_access_key"] + + # Create a Bucket object + bucket = Bucket( + name=account_id, + account_id=account_id, + access_key_id=read_access_key_id, + secret_access_key=read_secret_access_key, + ) + logger.debug(f"Parsed bucket from {env_file}: {bucket}") + return bucket + except KeyError as e: + logger.error(f"Missing key in {env_file}: {e}") + raise e diff --git a/src/tplr/comms.py b/src/tplr/comms.py new file mode 100644 index 0000000..a9aed5c --- /dev/null +++ b/src/tplr/comms.py @@ -0,0 +1,691 @@ +import os +import time +import torch +import asyncio +import aiofiles +import tempfile +import numpy as np +import bittensor as bt +from typing import List, Dict, Optional, Tuple +from types import SimpleNamespace +from aiobotocore.session import get_session +from . import __version__ +from .config import client_config, BUCKET_SECRETS +from .chain import ChainManager +from .schemas import Bucket + +import tplr as tplr +import botocore + +# Constants +CF_REGION_NAME: str = "enam" +LOCAL_TMP_DIR = "/tmp/local_store" + + +class Comms(ChainManager): + def __init__( + self, + wallet: "bt.wallet", + save_location: str = "/tmp", + key_prefix: str = "model", + **kwargs, + ): + self.wallet = wallet + # Get the bucket directly + self.bucket = self.get_own_bucket() + # Now initialize ChainManager with the bucket + super().__init__( + config=kwargs.get("config"), + netuid=kwargs.get("netuid"), + metagraph=kwargs.get("metagraph"), + hparams=kwargs.get("hparams"), + wallet=self.wallet, + bucket=self.bucket, + ) + + # Use the hotkey directly in the save_location + hotkey = self.wallet.hotkey.ss58_address + self.save_location = os.path.join("/tmp", f"hotkey_{hotkey}") + os.makedirs(self.save_location, exist_ok=True) + self.key_prefix = key_prefix + self.session = get_session() + self.lock = asyncio.Lock() + + def get_own_bucket(self) -> Bucket: + """Gets bucket configuration from environment variables via config.BUCKET_SECRETS.""" + try: + # Create a Bucket object using write credentials from BUCKET_SECRETS + bucket = Bucket( + name=BUCKET_SECRETS["account_id"], + account_id=BUCKET_SECRETS["account_id"], + access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) + tplr.logger.debug(f"Created bucket from environment: {bucket}") + return bucket + + except KeyError as e: + tplr.logger.error(f"Missing required R2 configuration: {e}") + raise + except Exception as e: + tplr.logger.error(f"Error creating bucket: {e}") + raise + + def get_base_url(self, account_id): + """Constructs the base URL for the R2 storage endpoint.""" + return f"https://{account_id}.r2.cloudflarestorage.com" + + def delete_local_directory(self, path: str): + """Safely remove a local directory and all its contents.""" + if not os.path.exists(path): + return + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(path) + + # Convert all the existing functions to methods + async def cleanup_local_data( + self, uid: str, current_window: int, stale_retention: int + ): + """Clean up stale local data for a given uid.""" + user_dir = os.path.join(LOCAL_TMP_DIR, str(uid)) + if not os.path.exists(user_dir): + return + + min_allowed_window = current_window - stale_retention + for wdir in os.listdir(user_dir): + if wdir.isdigit(): + w = int(wdir) + if w < min_allowed_window: + old_path = os.path.join(user_dir, wdir) + tplr.logger.debug(f"Removing stale local directory: {old_path}") + try: + self.delete_local_directory(old_path) + except Exception as e: + tplr.logger.debug( + f"Error removing stale directory {old_path}: {e}" + ) + + async def cleanup_s3_data( + self, uid: str, current_window: int, stale_retention: int + ): + """Clean up stale S3 data for a given uid.""" + min_allowed_window = current_window - stale_retention + prefix = f"{uid}/" + + session = get_session() + async with session.create_client( + "s3", + endpoint_url=self.get_base_url(BUCKET_SECRETS["account_id"]), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) as s3_client: + continuation_token = None + + while True: + list_args = { + "Bucket": BUCKET_SECRETS["bucket_name"], + "Prefix": prefix, + "MaxKeys": 1000, + } + if continuation_token: + list_args["ContinuationToken"] = continuation_token + + response = await s3_client.list_objects_v2(**list_args) + contents = response.get("Contents", []) + + # Identify stale objects to delete + stale_objects = [] + for obj in contents: + key = obj["Key"] + # Key format: uid/window/key + parts = key.split("/") + if len(parts) < 2: + continue + try: + w = int(parts[1]) + except ValueError: + continue + + if w < min_allowed_window: + stale_objects.append({"Key": key}) + + # Batch delete stale objects + if stale_objects: + tplr.logger.debug( + f"Removing stale S3 objects for {uid}: {stale_objects}" + ) + await s3_client.delete_objects( + Bucket=BUCKET_SECRETS["bucket_name"], + Delete={"Objects": stale_objects}, + ) + + if response.get("IsTruncated"): + continuation_token = response.get("NextContinuationToken") + else: + break + + async def s3_put_object(self, key: str, data: bytes): + """Upload object to S3.""" + session = get_session() + async with session.create_client( + "s3", + endpoint_url=self.get_base_url(BUCKET_SECRETS["account_id"]), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) as s3_client: + await s3_client.put_object( + Bucket=BUCKET_SECRETS["bucket_name"], Key=key, Body=data + ) + + async def s3_get_object(self, key: str, timeout: int) -> Optional[dict]: + """Download object from S3.""" + session = get_session() + async with session.create_client( + "s3", + endpoint_url=self.get_base_url(BUCKET_SECRETS["account_id"]), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) as s3_client: + try: + # Check if file exists first + try: + await s3_client.head_object( + Bucket=BUCKET_SECRETS["bucket_name"], Key=key + ) + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: + tplr.logger.debug(f"Object not found or access denied: {e}") + return None + + response = await asyncio.wait_for( + s3_client.get_object(Bucket=BUCKET_SECRETS["bucket_name"], Key=key), + timeout=timeout, + ) + except asyncio.TimeoutError: + tplr.logger.debug(f"Timeout occurred while downloading {key}.") + return None + except Exception as e: + tplr.logger.debug(f"An error occurred during GET {key}: {e}") + return None + + # Save to a temporary file and load + with tempfile.NamedTemporaryFile(delete=True, suffix=".pt") as temp_file: + temp_file_path = temp_file.name + async with aiofiles.open(temp_file_path, "wb") as outfile: + while True: + chunk = await response["Body"].read(1 * 1024 * 1024) + if not chunk: + break + await outfile.write(chunk) + + # Load the object + try: + with open(temp_file_path, "rb") as f: + state_dict = torch.load(f, weights_only=True) + return state_dict + except Exception as e: + tplr.logger.debug(f"Error loading state_dict from {key}: {e}") + return None + + async def put( + self, + state_dict: dict, + uid: str, + window: int, + key: str, + local: bool = True, + stale_retention: int = 10, + ): + """PUT operation: Store the state_dict either locally or in R2.""" + tplr.logger.debug(f"PUT {uid}/{window}/{key} -->") + + # Create versioned filename + filename = f"{key}-{window}-{uid}-v{__version__}.pt" + + # Create a temporary file path + temp_file_path = tempfile.mktemp(suffix=".pt") + + try: + # Save state_dict to the temporary file + torch.save(state_dict, temp_file_path) + + if local: + # Local storage logic remains unchanged + await self.cleanup_local_data( + uid=uid, current_window=window, stale_retention=stale_retention + ) + local_dir = os.path.join(LOCAL_TMP_DIR, str(uid), str(window)) + os.makedirs(local_dir, exist_ok=True) + final_path = os.path.join(local_dir, filename) + os.replace(temp_file_path, final_path) + else: + # Cleanup old S3 data + await self.cleanup_s3_data( + uid=uid, current_window=window, stale_retention=stale_retention + ) + + # Check file size + file_size = os.path.getsize(temp_file_path) + object_key = f"{uid}/{window}/{filename}" + + if file_size > 5 * 1024 * 1024 * 1024: # 5GB + # Use multipart upload for large files + success = await self.upload_large_file(temp_file_path, object_key) + if not success: + raise Exception("Large file upload failed") + else: + # Use regular upload for smaller files + async with aiofiles.open(temp_file_path, "rb") as f: + data = await f.read() + await self.s3_put_object(object_key, data) + + # Remove temporary file after successful upload + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + except Exception as e: + tplr.logger.debug(f"PUT error {uid}/{window}/{key}: {e}") + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + tplr.logger.debug(f"PUT {uid}/{window}/{key} <--") + + async def get( + self, + uid: str, + window: int, + key: str, + timeout: int = 30, + local: bool = True, + stale_retention: int = 10, + ) -> Optional[dict]: + """GET operation: Retrieve state_dict from local or R2 storage.""" + filename = f"{key}-{window}-{uid}-v{__version__}.pt" + full_key = f"{uid}/{window}/{filename}" + tplr.logger.debug(f"GET {full_key} -->") + + try: + if local: + # Local storage logic remains unchanged + await self.cleanup_local_data( + uid=uid, current_window=window, stale_retention=stale_retention + ) + local_path = os.path.join( + LOCAL_TMP_DIR, str(uid), str(window), filename + ) + if not os.path.exists(local_path): + tplr.logger.debug(f"Local file not found: {local_path}") + return None + state_dict = torch.load(local_path, weights_only=True) + return state_dict + else: + # Cleanup old S3 data + await self.cleanup_s3_data( + uid=uid, current_window=window, stale_retention=stale_retention + ) + + # Check file size first + async with self.session.create_client( + "s3", + endpoint_url=self.get_base_url(self.bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=self.bucket.access_key_id, + aws_secret_access_key=self.bucket.secret_access_key, + ) as s3_client: + try: + response = await s3_client.head_object( + Bucket=self.bucket.name, Key=full_key + ) + file_size = response["ContentLength"] + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: + tplr.logger.debug(f"Failed to get object metadata: {e}") + return None + + # Create a temporary file for download + with tempfile.NamedTemporaryFile( + delete=False, suffix=".pt" + ) as temp_file: + temp_file_path = temp_file.name + + try: + if file_size > 5 * 1024 * 1024 * 1024: # 5GB + # Use multipart download for large files + success = await self.download_large_file( + full_key, temp_file_path + ) + if not success: + raise Exception("Large file download failed") + else: + # Use regular download for smaller files + state_dict = await self.s3_get_object(full_key, timeout) + return state_dict + + # Load the state dict from the temporary file + state_dict = torch.load(temp_file_path, weights_only=True) + return state_dict + + finally: + # Clean up temporary file + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + except Exception as e: + tplr.logger.debug(f"GET error {full_key}: {e}") + return None + + finally: + tplr.logger.debug(f"GET {full_key} <--") + + async def get_with_retry( + self, + uid: str, + window: int, + key: str, + timeout: int, + local: bool = True, + stale_retention: int = 10, + ) -> Optional[dict]: + """GET with retry operation.""" + start_time = time.time() + end_time = start_time + timeout + + while True: + if time.time() >= end_time: + tplr.logger.debug(f"GET {uid}/{window}/{key} timed out.") + return None + + state_dict = await self.get( + uid=uid, + window=window, + key=key, + local=local, + stale_retention=stale_retention, + ) + if state_dict is not None: + return state_dict + + # Retry after a short delay + await asyncio.sleep(0.1) + + async def gather( + self, + state_dict: Dict[str, torch.Tensor], + my_uid: str, + uids: List[str], + window: int, + key: str, + timeout: int, + device: str, + local: bool = True, + stale_retention: int = 10, + ) -> SimpleNamespace: + """Gather operation.""" + start_time = time.time() + metrics = {"upload_bytes": 0, "download_bytes": 0, "successes": []} + + # Put own state_dict if available + if state_dict is not None: + await self.put( + state_dict=state_dict, + uid=str(my_uid), + window=window, + key=key, + local=local, + stale_retention=stale_retention, + ) + metrics["upload_bytes"] += sum( + tensor.element_size() * tensor.nelement() + for tensor in state_dict.values() + ) + + # Small delay to ensure data propagation + await asyncio.sleep(0.1) + + # Prepare gather tasks + gather_tasks = [ + self.get_with_retry( + uid=uid, + window=window, + key=key, + timeout=timeout, + local=local, + stale_retention=stale_retention, + ) + for uid in uids + ] + + # Initialize the aggregated state dict + aggregated_state_dict = {} + successes = [] + + # Process responses + responses = await asyncio.gather(*gather_tasks) + for idx, resp in enumerate(responses): + if resp is None: + successes.append(False) + continue + + successes.append(True) + + # Initialize aggregated_state_dict if empty + if not aggregated_state_dict: + aggregated_state_dict = { + param_name: [torch.zeros_like(tensor).to(device) for _ in uids] + for param_name, tensor in resp.items() + } + + # Fill in data from this response + for param_name, tensor in resp.items(): + aggregated_state_dict[param_name][idx] = tensor.to(device) + metrics["download_bytes"] += tensor.element_size() * tensor.nelement() + + # Calculate success metrics + success_rate = sum(successes) / len(successes) if successes else 0 + total_time = time.time() - start_time + + return SimpleNamespace( + time=total_time, + upload_bytes=metrics["upload_bytes"], + download_bytes=metrics["download_bytes"], + success_rate=success_rate, + successes=successes, + state_dict=aggregated_state_dict, + ) + + async def upload_large_file(self, file_path: str, filename: str) -> bool: + """ + Uploads a large file to R2 using multipart upload. + + Args: + file_path (str): Path to the local file to upload + filename (str): Destination filename in R2 + + Returns: + bool: True if successful, False otherwise + """ + try: + # Use 16MB chunks for multipart upload + chunk_size = 16 * 1024 * 1024 + + async with self.session.create_client( + "s3", + endpoint_url=self.get_base_url(self.bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=self.bucket.access_key_id, + aws_secret_access_key=self.bucket.secret_access_key, + ) as s3_client: + # Initialize multipart upload + response = await s3_client.create_multipart_upload( + Bucket=self.bucket.name, Key=filename + ) + upload_id = response["UploadId"] + + # Upload parts + parts = [] + part_number = 1 + + async with aiofiles.open(file_path, "rb") as f: + while True: + data = await f.read(chunk_size) + if not data: + break + + response = await s3_client.upload_part( + Bucket=self.bucket.name, + Key=filename, + PartNumber=part_number, + UploadId=upload_id, + Body=data, + ) + + parts.append( + {"PartNumber": part_number, "ETag": response["ETag"]} + ) + part_number += 1 + + # Complete multipart upload + await s3_client.complete_multipart_upload( + Bucket=self.bucket.name, + Key=filename, + UploadId=upload_id, + MultipartUpload={"Parts": parts}, + ) + + tplr.logger.debug(f"Successfully uploaded large file {filename}") + return True + + except Exception as e: + tplr.logger.error(f"Error uploading large file {filename}: {e}") + return False + + async def download_large_file(self, filename: str, destination_path: str) -> bool: + """ + Downloads a large file from R2 using multipart download. + + Args: + filename (str): File to download from R2 + destination_path (str): Local path to save the file + + Returns: + bool: True if successful, False otherwise + """ + try: + async with self.session.create_client( + "s3", + endpoint_url=self.get_base_url(self.bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=self.bucket.access_key_id, + aws_secret_access_key=self.bucket.secret_access_key, + ) as s3_client: + # Get file size + response = await s3_client.head_object( + Bucket=self.bucket.name, Key=filename + ) + file_size = response["ContentLength"] + + # Use 16MB chunks for multipart download + chunk_size = 16 * 1024 * 1024 + total_parts = (file_size + chunk_size - 1) // chunk_size + + async with aiofiles.open(destination_path, "wb") as f: + for part in range(total_parts): + start = part * chunk_size + end = min(start + chunk_size, file_size) + + response = await s3_client.get_object( + Bucket=self.bucket.name, + Key=filename, + Range=f"bytes={start}-{end-1}", + ) + + chunk = await response["Body"].read() + await f.write(chunk) + + tplr.logger.debug(f"Successfully downloaded large file {filename}") + return True + + except Exception as e: + tplr.logger.error(f"Error downloading large file {filename}: {e}") + return False + + async def cleanup_old_checkpoints(self, keep_last: int = 3): + """ + Removes old checkpoints from storage, keeping only the most recent ones. + + Args: + keep_last (int): Number of most recent checkpoints to keep + """ + try: + async with self.session.create_client( + "s3", + endpoint_url=self.get_base_url(self.bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=self.bucket.access_key_id, + aws_secret_access_key=self.bucket.secret_access_key, + ) as s3_client: + # List all checkpoint files + paginator = s3_client.get_paginator("list_objects_v2") + checkpoint_files = [] + + async for page in paginator.paginate( + Bucket=self.bucket.name, Prefix="checkpoint" + ): + for obj in page.get("Contents", []): + if obj["Key"].startswith("checkpoint"): + checkpoint_files.append(obj) + + # Sort by last modified time + checkpoint_files.sort(key=lambda x: x["LastModified"], reverse=True) + + # Delete older checkpoints + if len(checkpoint_files) > keep_last: + to_delete = checkpoint_files[keep_last:] + await s3_client.delete_objects( + Bucket=self.bucket.name, + Delete={"Objects": [{"Key": obj["Key"]} for obj in to_delete]}, + ) + tplr.logger.info(f"Deleted {len(to_delete)} old checkpoints") + + except Exception as e: + tplr.logger.error(f"Error cleaning up old checkpoints: {e}") + + def get_highest_stake_validator(self) -> Tuple[Optional[int], float]: + """Returns the UID and stake of the neuron with the highest stake.""" + stakes = self.metagraph.S + tplr.logger.info(stakes) + + # Convert numpy array to torch tensor if needed + if isinstance(stakes, np.ndarray): + stakes = torch.from_numpy(stakes) + + # Check if any stakes are non-zero + if torch.all(stakes == 0): + return None, 0.0 + + highest_stake_uid = torch.argmax(stakes).item() + stake = stakes[highest_stake_uid].item() + + # Validate the stake is actually non-zero + if stake == 0: + return None, 0.0 + + return highest_stake_uid, stake diff --git a/src/tplr/compress.py b/src/tplr/compress.py new file mode 100644 index 0000000..4955f42 --- /dev/null +++ b/src/tplr/compress.py @@ -0,0 +1,306 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Adapted from https://github.com/bloc97/DeMo and NousResearch +# Original implementation by bloc97 (https://github.com/bloc97/DeMo) + + +# Global imports +import math +import torch +import torch.fft +from einops import rearrange + + +class TransformDCT: + @torch.no_grad() + def __init__(self, model, target_chunk, norm="ortho"): + self.target_chunk = target_chunk + + self.shape_dict = dict() + self.f_dict = dict() + self.b_dict = dict() + + # Get all variants of model tensor sizes + # Generate all possible valid DCT sizes for model tensors + for _, p in model.named_parameters(): + if not p.requires_grad: + continue + for s in p.shape: + # Get the closest smallest divisor to the targeted DCT size + sc = _get_smaller_split(s, self.target_chunk) + self.shape_dict[s] = sc + + # Pregenerate DCT basis matrices + if sc not in self.f_dict: + identity_matrix = torch.eye(sc) + self.f_dict[sc] = _dct(identity_matrix, norm=norm).to(p.dtype).to(p.device) + self.b_dict[sc] = _idct(identity_matrix, norm=norm).to(p.dtype).to(p.device) + + @torch.no_grad() + def einsum_2d(self, x, b, d=None): + if d is None: + return torch.einsum("...ij, jb -> ...ib", x, b) + else: + # Note: b-c axis output is transposed to chunk DCT in 2D + return torch.einsum("...ijkl, jb, ld -> ...ikbd", x, b, d) + + @torch.no_grad() + def einsum_2d_t(self, x, b, d=None): + if d is None: + return torch.einsum("...ij, jb -> ...ib", x, b) + else: + # Note: b-c axis output is transposed to chunk DCT in 2D + return torch.einsum("...ijkl, kb, ld -> ...ibjd", x, b, d) + + @torch.no_grad() + def encode(self, x): + if len(x.shape) > 1: # 2D weights + n1 = self.shape_dict[x.shape[0]] + n2 = self.shape_dict[x.shape[1]] + n1w = self.f_dict[n1].to(x.device) + n2w = self.f_dict[n2].to(x.device) + self.f_dict[n1] = n1w + self.f_dict[n2] = n2w + + x = rearrange(x, "(y h) (x w) -> y h x w", h=n1, w=n2) + x = self.einsum_2d(x, n1w, n2w) + + else: # 1D weights + n1 = self.shape_dict[x.shape[0]] + n1w = self.f_dict[n1].to(x.device) + self.f_dict[n1] = n1w + + x = rearrange(x, "(x w) -> x w", w=n1) + x = self.einsum_2d(x, n1w) + + return x + + @torch.no_grad() + def decode(self, x): + if len(x.shape) > 2: # 2D weights + n1 = x.shape[2] + n2 = x.shape[3] + n1w = self.b_dict[n1].to(x.device) + n2w = self.b_dict[n2].to(x.device) + self.b_dict[n1] = n1w + self.b_dict[n2] = n2w + + x = self.einsum_2d_t(x, n1w, n2w) + x = rearrange(x, "y h x w -> (y h) (x w)") + + else: # 1D weights + n1 = x.shape[1] + n1w = self.b_dict[n1].to(x.device) + self.b_dict[n1] = n1w + + x = self.einsum_2d_t(x, n1w) + x = rearrange(x, "x w -> (x w)") + + return x + + +class CompressDCT: + @torch.no_grad() + def __init__(self): + pass + + def _clamp_topk(self, x, topk): + if topk > x.shape[-1]: + topk = x.shape[-1] + if topk < 1: + topk = 1 + return topk + + @torch.no_grad() + def compress(self, x, topk): + xshape = x.shape + if len(x.shape) > 2: # 2D weights + x = rearrange(x, "y x h w -> y x (h w)") + + # Limit topk to max size + totalk = x.shape[-1] + topk = self._clamp_topk(x, topk) + + idx = torch.topk(x.abs(), k=topk, dim=-1, largest=True, sorted=False).indices + val = torch.gather(x, dim=-1, index=idx) + + return idx, val, xshape, totalk + + @torch.no_grad() + def decompress(self, p, idx, val, xshape, totalk): + x = torch.zeros(xshape, device=p.device, dtype=p.dtype) + + if len(xshape) > 2: # 2D weights + x = rearrange(x, "y x h w -> y x (h w)") + + # TODO: Careful, this is nondeterministic across different CUDA devices! might cause errors to accumulate between nodes! + x.scatter_reduce_(dim=-1, index=idx, src=val, reduce="mean", include_self=False).reshape(xshape) + + if len(x.shape) > 2: # 2D weights + x = rearrange(x, "y x (h w) -> y x h w", h=xshape[2]) + + return x + + @torch.no_grad() + def batch_decompress(self, p, idx, val, xshape, totalk): + idx = torch.concatenate(idx, dim=-1).to(device=p.device) + val = torch.concatenate(val, dim=-1).to(device=p.device) + return self.decompress(p, idx, val, xshape, totalk) + + +# Code modified and sourced from https://github.com/zh217/torch-dct +def _dct_fft_impl(v): + return torch.view_as_real(torch.fft.fft(v, dim=1)) + + +def _idct_irfft_impl(V): + return torch.fft.irfft(torch.view_as_complex(V), n=V.shape[1], dim=1) + + +def _dct(x, norm=None): + """ + Discrete Cosine Transform, Type II (a.k.a. the DCT) + + For the meaning of the parameter `norm`, see: + https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.fftpack.dct.html + + :param x: the input signal + :param norm: the normalization, None or 'ortho' + :return: the DCT-II of the signal over the last dimension + """ + x_shape = x.shape + N = x_shape[-1] + x = x.contiguous().view(-1, N) + + v = torch.cat([x[:, ::2], x[:, 1::2].flip([1])], dim=1) + + Vc = _dct_fft_impl(v) + + k = -torch.arange(N, dtype=x.dtype, device=x.device)[None, :] * math.pi / (2 * N) + W_r = torch.cos(k) + W_i = torch.sin(k) + + V = Vc[:, :, 0] * W_r - Vc[:, :, 1] * W_i + + if norm == "ortho": + V[:, 0] /= math.sqrt(N) * 2 + V[:, 1:] /= math.sqrt(N / 2) * 2 + + V = 2 * V.view(*x_shape) + + return V + + +def _idct(X, norm=None): + """ + The inverse to DCT-II, which is a scaled Discrete Cosine Transform, Type III + + Our definition of idct is that idct(dct(x)) == x + + For the meaning of the parameter `norm`, see: + https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.fftpack.dct.html + + :param X: the input signal + :param norm: the normalization, None or 'ortho' + :return: the inverse DCT-II of the signal over the last dimension + """ + + x_shape = X.shape + N = x_shape[-1] + + X_v = X.contiguous().view(-1, x_shape[-1]) / 2 + + if norm == "ortho": + X_v[:, 0] *= math.sqrt(N) * 2 + X_v[:, 1:] *= math.sqrt(N / 2) * 2 + + k = torch.arange(x_shape[-1], dtype=X.dtype, device=X.device)[None, :] * math.pi / (2 * N) + W_r = torch.cos(k) + W_i = torch.sin(k) + + V_t_r = X_v + V_t_i = torch.cat([X_v[:, :1] * 0, -X_v.flip([1])[:, :-1]], dim=1) + + V_r = V_t_r * W_r - V_t_i * W_i + V_i = V_t_r * W_i + V_t_i * W_r + + V = torch.cat([V_r.unsqueeze(2), V_i.unsqueeze(2)], dim=2) + + v = _idct_irfft_impl(V) + x = v.new_zeros(v.shape) + x[:, ::2] += v[:, : N - (N // 2)] + x[:, 1::2] += v.flip([1])[:, : N // 2] + + return x.view(*x_shape) + + +def _get_prime_divisors(n): + divisors = [] + while n % 2 == 0: + divisors.append(2) + n //= 2 + while n % 3 == 0: + divisors.append(3) + n //= 3 + i = 5 + while i * i <= n: + for k in (i, i + 2): + while n % k == 0: + divisors.append(k) + n //= k + i += 6 + if n > 1: + divisors.append(n) + return divisors + + +def _get_divisors(n): + divisors = [] + if n == 1: + divisors.append(1) + elif n > 1: + prime_factors = _get_prime_divisors(n) + divisors = [1] + last_prime = 0 + factor = 0 + slice_len = 0 + # Find all the products that are divisors of n + for prime in prime_factors: + if last_prime != prime: + slice_len = len(divisors) + factor = prime + else: + factor *= prime + for i in range(slice_len): + divisors.append(divisors[i] * factor) + last_prime = prime + divisors.sort() + return divisors + + +def _get_smaller_split(n, close_to): + all_divisors = _get_divisors(n) + for ix, val in enumerate(all_divisors): + if val == close_to: + return val + if val > close_to: + if ix == 0: + return val + return all_divisors[ix - 1] + return n diff --git a/src/tplr/config.py b/src/tplr/config.py new file mode 100644 index 0000000..ddd5cc2 --- /dev/null +++ b/src/tplr/config.py @@ -0,0 +1,58 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import os +import sys +import botocore.config + +# Local imports +from .logging import logger + +# Configure bucket secrets from environment variables +BUCKET_SECRETS = { + "account_id": os.environ.get("R2_ACCOUNT_ID"), + "bucket_name": os.environ.get("R2_ACCOUNT_ID"), # Using account_id as bucket name + "read": { + "access_key_id": os.environ.get("R2_READ_ACCESS_KEY_ID"), + "secret_access_key": os.environ.get("R2_READ_SECRET_ACCESS_KEY") + }, + "write": { + "access_key_id": os.environ.get("R2_WRITE_ACCESS_KEY_ID"), + "secret_access_key": os.environ.get("R2_WRITE_SECRET_ACCESS_KEY") + } +} + +# Validate required environment variables +required_vars = [ + "R2_ACCOUNT_ID", + "R2_READ_ACCESS_KEY_ID", + "R2_READ_SECRET_ACCESS_KEY", + "R2_WRITE_ACCESS_KEY_ID", + "R2_WRITE_SECRET_ACCESS_KEY" +] + +missing_vars = [var for var in required_vars if not os.environ.get(var)] +if missing_vars: + logger.error(f"Missing required environment variables: {', '.join(missing_vars)}") + sys.exit(1) + +# Configure the S3 client +client_config = botocore.config.Config( + max_pool_connections=256, +) diff --git a/src/templar/dataset.py b/src/tplr/dataset.py similarity index 98% rename from src/templar/dataset.py rename to src/tplr/dataset.py index 208e233..efffc19 100644 --- a/src/templar/dataset.py +++ b/src/tplr/dataset.py @@ -2,28 +2,28 @@ # © 2024 templar.tech # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +# fmt: off # Global imports +import random +import typing import asyncio import aiohttp import numpy as np -import random -import typing -from torch.utils.data import IterableDataset from transformers import AutoTokenizer - +from torch.utils.data import IterableDataset class SubsetLoader(IterableDataset): """ diff --git a/src/tplr/hparams.py b/src/tplr/hparams.py new file mode 100644 index 0000000..34a29fd --- /dev/null +++ b/src/tplr/hparams.py @@ -0,0 +1,139 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import json +from types import SimpleNamespace +from transformers import AutoTokenizer, LlamaConfig + +# Local imports +from .logging import logger + +DEFAULT_HPARAMS = { + # Run configuration + "spec_version": 5, + "project": "templar", + + # Model parameters + "sequence_length": 1024, + "pages_per_window": 2, + "batch_size": 8, + "learning_rate": 0.001, + + # Window and sync parameters + "blocks_per_window": 2, + "windows_per_sync": 100, + "windows_per_weights": 10, + + # Optimization parameters + "momentum_decay": 0.999, + "topk_compression": 32, + "target_chunk": 64, + "scores_alpha": 0.001, + + # Model architecture (these should be in your hparams.json) + "tokenizer_name": "huggyllama/llama-7b", + "hidden_size": 4096, + "num_hidden_layers": 32, + "num_attention_heads": 32, + "intermediate_size": 11008, + "num_key_value_heads": 32, + "activation_function": "silu", + "max_position_embeddings": 2048, + + # Bucket configuration + "bucket_name": "your-default-bucket-name", + + # Scheduler parameters + "warmup_steps": 250, + "alpha_f": 0.1, # Final learning rate multiplier + "t_max": 20000, # Total steps for cosine decay +} + +def create_namespace(hparams: dict) -> SimpleNamespace: + """ + Create a SimpleNamespace from the hyperparameters and add model configuration. + + Args: + hparams (dict): Hyperparameters dictionary. + + Returns: + SimpleNamespace: Namespace containing hyperparameters and model configuration. + """ + # Merge with defaults + full_hparams = DEFAULT_HPARAMS.copy() + full_hparams.update(hparams) + + hparams_ns = SimpleNamespace(**full_hparams) + + # Initialize tokenizer + try: + hparams_ns.tokenizer = AutoTokenizer.from_pretrained( + hparams_ns.tokenizer_name, verbose=False, clean_up_tokenization_spaces=True + ) + hparams_ns.tokenizer.pad_token = hparams_ns.tokenizer.eos_token + except Exception as e: + logger.error(f"Failed to load tokenizer: {e}") + raise + + # Initialize model config + try: + hparams_ns.model_config = LlamaConfig( + vocab_size=hparams_ns.tokenizer.vocab_size, + hidden_size=hparams_ns.hidden_size, + num_hidden_layers=hparams_ns.num_hidden_layers, + num_attention_heads=hparams_ns.num_attention_heads, + intermediate_size=hparams_ns.intermediate_size, + num_key_value_heads=hparams_ns.num_key_value_heads, + activation_function=hparams_ns.activation_function, + max_position_embeddings=hparams_ns.max_position_embeddings, + ) + except Exception as e: + logger.error(f"Failed to create model config: {e}") + raise + + return hparams_ns + +def load_hparams(hparams_file: str = "hparams.json") -> SimpleNamespace: + """ + Load hyperparameters from a JSON file. + + Args: + hparams_file (str): Path to the hyperparameters JSON file. + + Returns: + SimpleNamespace: A namespace containing the hyperparameters and model configuration. + + Example: + hparams = load_hparams() + print(hparams.hidden_size) + print(hparams.model_config) + """ + try: + with open(hparams_file, "r") as f: + hparams = json.load(f) + return create_namespace(hparams) + except FileNotFoundError: + logger.warning(f"No {hparams_file} found, using default hyperparameters") + return create_namespace({}) + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in {hparams_file}: {e}") + raise + except Exception as e: + logger.error(f"Error loading hyperparameters: {e}") + raise diff --git a/src/templar/logging.py b/src/tplr/logging.py similarity index 91% rename from src/templar/logging.py rename to src/tplr/logging.py index e0b231b..89fbe17 100644 --- a/src/templar/logging.py +++ b/src/tplr/logging.py @@ -2,25 +2,25 @@ # © 2024 templar.tech # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +# fmt: off # Global imports -import logging import time -from rich.highlighter import NullHighlighter +import logging from rich.logging import RichHandler - +from rich.highlighter import NullHighlighter def T() -> float: """ diff --git a/src/tplr/schemas.py b/src/tplr/schemas.py new file mode 100644 index 0000000..f1338fc --- /dev/null +++ b/src/tplr/schemas.py @@ -0,0 +1,44 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +from pydantic import BaseModel + + +class Bucket(BaseModel): + """Configuration for a bucket, including name and access credentials.""" + + def __hash__(self): + # Use all fields to generate a unique hash + return hash( + (self.name, self.account_id, self.access_key_id, self.secret_access_key) + ) + + def __eq__(self, other): + # Compare all fields to determine equality + if isinstance(other, Bucket): + return self.dict() == other.dict() + return False + + name: str + account_id: str + access_key_id: str + secret_access_key: str + class Config: + str_min_length = 1 + str_strip_whitespace = True diff --git a/src/tplr/wandb.py b/src/tplr/wandb.py new file mode 100644 index 0000000..4f4a4bc --- /dev/null +++ b/src/tplr/wandb.py @@ -0,0 +1,88 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +import os +import wandb +from wandb.sdk.wandb_run import Run +from . import __version__ +from .logging import logger + + +def initialize_wandb( + run_prefix: str, uid: str, config: any, group: str, job_type: str +) -> Run: + """Initialize WandB run with persistence and resumption capabilities. + + Args: + run_prefix (str): Prefix for the run name (e.g., 'V' for validator, 'M' for miner) + uid (str): Unique identifier for the run + config (any): Configuration object containing project and other settings + group (str): Group name for organizing runs + job_type (str): Type of job (e.g., 'validation', 'training') + + Returns: + Run: Initialized WandB run object + """ + # Ensure the wandb directory exists + wandb_dir = os.path.join(os.getcwd(), "wandb") + os.makedirs(wandb_dir, exist_ok=True) + + # Define the run ID file path inside the wandb directory + run_id_file = os.path.join( + wandb_dir, f"wandb_run_id_{run_prefix}{uid}_{__version__}.txt" + ) + + # Check for existing run and verify it still exists in wandb + run_id = None + if os.path.exists(run_id_file): + with open(run_id_file, "r") as f: + run_id = f.read().strip() + + # Verify if run still exists in wandb + try: + api = wandb.Api() + api.run(f"tplr/{config.project}-v{__version__}/{run_id}") + logger.info(f"Found existing run ID: {run_id}") + except Exception: + logger.info(f"Previous run {run_id} not found in WandB, starting new run") + run_id = None + os.remove(run_id_file) + + # Initialize WandB + run = wandb.init( + project=f"{config.project}-v{__version__}", + entity="tplr", + id=run_id, + resume="must" if run_id else "never", + name=f"{run_prefix}{uid}", + config=config, + group=group, + job_type=job_type, + dir=wandb_dir, + settings=wandb.Settings( + init_timeout=300, + _disable_stats=True, + ), + ) + + # Special handling for evaluator + if run_prefix == "E": + tasks = config.tasks.split(",") + for task in tasks: + metric_name = f"eval/{task}" + wandb.define_metric( + name=metric_name, step_metric="global_step", plot=True, summary="max" + ) + + # Save run ID for future resumption + if not run_id: + with open(run_id_file, "w") as f: + f.write(run.id) + + return run + + +# TODO: Add error handling for network issues +# TODO: Add retry mechanism for wandb initialization +# TODO: Add cleanup mechanism for old run ID files +# TODO: Add support for custom wandb settings diff --git a/tests/test_autoupdater.py b/tests/test_autoupdater.py deleted file mode 100644 index dcf2fca..0000000 --- a/tests/test_autoupdater.py +++ /dev/null @@ -1,260 +0,0 @@ -# ruff: noqa -# pylint: disable=all -# mypy: ignore-errors - -import os -import tempfile -from unittest.mock import MagicMock, patch, PropertyMock - -import git -import pytest -import asyncio -import subprocess -import json - -# Import the AutoUpdate class -from templar.autoupdate import AutoUpdate, TARGET_BRANCH - - -@pytest.mark.asyncio -async def test_autoupdate_cleanup_old_versions(): - """Test cleanup of old versions.""" - autoupdater = AutoUpdate() - - with patch("templar.autoupdate.BUCKET_SECRETS", {"bucket_name": "test-bucket"}): - with patch("templar.autoupdate.delete_old_version_files") as mock_cleanup: - # Mock the templar.__version__ to a known value - with patch("templar.__version__", "0.1.29"): - await autoupdater.cleanup_old_versions() - mock_cleanup.assert_called_with("test-bucket", "0.1.29") - - -def test_autoupdate_run_method(): - """Test the run method's loop.""" - autoupdater = AutoUpdate() - - with patch.object(autoupdater, "try_update") as mock_try_update: - # Create a side effect function to stop the loop after first iteration - def side_effect_sleep(duration): - raise KeyboardInterrupt # Breaking the infinite loop - - with patch("time.sleep", side_effect=side_effect_sleep) as mock_sleep: - try: - autoupdater.run() - except KeyboardInterrupt: - pass # Expected to break the loop - - mock_try_update.assert_called_once() - mock_sleep.assert_called_once_with(60) - - -def test_autoupdate_restart_app_pm2_no_process_name_failure(): - """Test that the application handles failure to get PM2 process name.""" - autoupdater = AutoUpdate() - - # Mock get_pm2_process_name to return None - with patch.object(autoupdater, "get_pm2_process_name", return_value=None): - with patch("templar.autoupdate.logger") as mock_logger: - with patch("templar.autoupdate.sys.exit", side_effect=SystemExit): - # Mock PM2 environment - with patch.dict(os.environ, {"PM2_HOME": "/path/to/pm2"}): - with pytest.raises(SystemExit): - autoupdater.restart_app() - # Check the correct info message is logged - mock_logger.info.assert_any_call( - "PM2 process name not found. Performing regular restart using subprocess.Popen" - ) - - -def test_autoupdate_restart_app_pm2_success(): - """Test that the application restarts successfully in a PM2 environment.""" - autoupdater = AutoUpdate() - - mock_pm2_process_name = "test_process" - # Mock get_pm2_process_name to return a test process name - with patch.object( - autoupdater, "get_pm2_process_name", return_value=mock_pm2_process_name - ): - with patch("templar.autoupdate.subprocess.run") as mock_run: - with patch("templar.autoupdate.sys.exit", side_effect=SystemExit): - # Mock PM2 environment - with patch.dict(os.environ, {"PM2_HOME": "/path/to/pm2"}): - with pytest.raises(SystemExit): - autoupdater.restart_app() - mock_run.assert_called_with( - ["pm2", "restart", mock_pm2_process_name], check=True - ) - - -@pytest.mark.asyncio -async def test_autoupdate_check_version_updated(): - """Test that check_version_updated works correctly when update is needed.""" - autoupdater = AutoUpdate() - - # Mock get_remote_version to return a higher version - async def mock_get_remote_version(self): - return "0.2.0" - - with patch.object(AutoUpdate, "get_remote_version", new=mock_get_remote_version): - # Mock templar.__version__ to a lower version - with patch("templar.__version__", "0.1.0"): - is_updated = await autoupdater.check_version_updated() - assert is_updated is True - - -@pytest.mark.asyncio -async def test_autoupdate_check_version_not_updated(): - """Test that check_version_updated works correctly when no update is needed.""" - autoupdater = AutoUpdate() - - # Mock get_remote_version to return the same version - async def mock_get_remote_version(self): - return "0.1.0" - - with patch.object(AutoUpdate, "get_remote_version", new=mock_get_remote_version): - # Mock templar.__version__ to the same version - with patch("templar.__version__", "0.1.0"): - is_updated = await autoupdater.check_version_updated() - assert is_updated is False - - -def test_autoupdate_attempt_update_success(): - """Test that attempt_update succeeds when repo is clean.""" - autoupdater = AutoUpdate() - - # Patch 'is_detached' property - with patch.object( - type(autoupdater.repo.head), "is_detached", new_callable=PropertyMock - ) as mock_is_detached: - mock_is_detached.return_value = False - - # Patch 'active_branch' property to return a mock branch - mock_branch = MagicMock() - mock_branch.name = TARGET_BRANCH - with patch.object( - type(autoupdater.repo), "active_branch", new_callable=PropertyMock - ) as mock_active_branch: - mock_active_branch.return_value = mock_branch - - with patch.object(autoupdater.repo, "is_dirty", return_value=False): - # Mock the 'remote' method to return a mock 'origin' - mock_origin = MagicMock() - mock_origin.fetch.return_value = None # Simulate successful fetch - with patch.object(autoupdater.repo, "remote", return_value=mock_origin): - # Mock 'git' attribute - mock_git = MagicMock() - autoupdater.repo.git = mock_git - - # Mock commits for local and remote to match - mock_commit = MagicMock() - mock_commit.hexsha = "abcdef" - with patch.object( - autoupdater.repo, "commit", return_value=mock_commit - ): - result = autoupdater.attempt_update() - mock_origin.fetch.assert_called_once() - mock_git.reset.assert_called_with( - "--hard", f"origin/{TARGET_BRANCH}" - ) - assert result is True - - -def test_autoupdate_attempt_update_dirty_repo(): - """Test that attempt_update fails when repo is dirty.""" - autoupdater = AutoUpdate() - - with patch.object(autoupdater.repo, "is_dirty", return_value=True): - with patch("templar.autoupdate.logger") as mock_logger: - result = autoupdater.attempt_update() - mock_logger.error.assert_called_with( - "Repository has uncommitted changes or untracked files. Cannot update." - ) - assert result is False - - -def test_autoupdate_attempt_update_pull_failure(): - """Test that attempt_update handles fetch failure.""" - autoupdater = AutoUpdate() - - # Patch 'is_detached' property - with patch.object( - type(autoupdater.repo.head), "is_detached", new_callable=PropertyMock - ) as mock_is_detached: - mock_is_detached.return_value = False - - # Patch 'active_branch' property - mock_branch = MagicMock() - mock_branch.name = TARGET_BRANCH - with patch.object( - type(autoupdater.repo), "active_branch", new_callable=PropertyMock - ) as mock_active_branch: - mock_active_branch.return_value = mock_branch - - with patch.object(autoupdater.repo, "is_dirty", return_value=False): - mock_origin = MagicMock() - mock_origin.fetch.side_effect = git.exc.GitCommandError( - "fetch", "Failed to fetch" - ) - with patch.object(autoupdater.repo, "remote", return_value=mock_origin): - # Mock 'git' attribute - mock_git = MagicMock() - autoupdater.repo.git = mock_git - - with patch("templar.autoupdate.logger") as mock_logger: - result = autoupdater.attempt_update() - # Confirm that an error was logged - mock_logger.error.assert_called_once() - error_msg = mock_logger.error.call_args[0][0] - assert error_msg.startswith("Git command failed:") - assert "Failed to fetch" in error_msg - assert result is False - - -def test_autoupdate_attempt_package_update(): - """Test that attempt_package_update calls the correct subprocess.""" - autoupdater = AutoUpdate() - - with patch("templar.autoupdate.subprocess.check_call") as mock_check_call: - autoupdater.attempt_package_update() - mock_check_call.assert_called_with( - ["uv", "sync", "--extra", "all"], - timeout=300, - ) - - -def test_autoupdate_attempt_package_update_failure(): - """Test that package update handles failures gracefully.""" - autoupdater = AutoUpdate() - - with patch( - "templar.autoupdate.subprocess.check_call", - side_effect=subprocess.CalledProcessError(1, "uv"), - ): - with patch("templar.autoupdate.logger") as mock_logger: - autoupdater.attempt_package_update() - mock_logger.exception.assert_called_once() - - -def test_autoupdate_get_pm2_process_name(): - """Test getting the PM2 process name.""" - autoupdater = AutoUpdate() - - # Mock os.getpid() - with patch("os.getpid", return_value=12345): - # Mock subprocess.run() - mock_pm2_output = json.dumps( - [ - {"name": "test_process", "pid": 12345}, - {"name": "other_process", "pid": 67890}, - ] - ) - - mock_completed_process = subprocess.CompletedProcess( - args=["pm2", "jlist"], - returncode=0, - stdout=mock_pm2_output, - ) - - with patch("subprocess.run", return_value=mock_completed_process): - process_name = autoupdater.get_pm2_process_name() - assert process_name == "test_process" diff --git a/tests/test_checkpoints.py b/tests/test_checkpoints.py deleted file mode 100644 index 70b9603..0000000 --- a/tests/test_checkpoints.py +++ /dev/null @@ -1,216 +0,0 @@ -# ruff: noqa -# pylint: disable=all -# mypy: ignore-errors -# type: ignore - -import asyncio -import os -import torch -import unittest -from unittest import mock -import tempfile -import glob -from aiobotocore.session import get_session - -from templar.checkpoint import ( - CheckpointManager, - get_base_url, - load_checkpoint, - download_checkpoint_from_neuron, -) -from templar import __version__ -from templar.config import BUCKET_SECRETS -from templar.constants import CF_REGION_NAME - - -class DummyModel(torch.nn.Module): - def __init__(self): - super().__init__() - self.linear = torch.nn.Linear(10, 1) - - -async def upload_exists_in_s3(bucket_name, key, access_key, secret_key, endpoint_url): - session = get_session() - async with session.create_client( - "s3", - endpoint_url=endpoint_url, - region_name=CF_REGION_NAME, - aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - ) as s3_client: - try: - await s3_client.head_object(Bucket=bucket_name, Key=key) - return True - except Exception: - return False - - -async def async_delete_from_s3(bucket_name, key, access_key, secret_key, endpoint_url): - session = get_session() - async with session.create_client( - "s3", - endpoint_url=endpoint_url, - region_name=CF_REGION_NAME, - aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - ) as s3_client: - try: - await s3_client.delete_object(Bucket=bucket_name, Key=key) - except Exception as e: - print(f"Error deleting S3 object: {e}") - - -class TestCheckpointManager(unittest.IsolatedAsyncioTestCase): - def setUp(self): - self.temp_dir = tempfile.TemporaryDirectory() - self.checkpoint_path = os.path.join(self.temp_dir.name, "checkpoint.pth") - self.model = DummyModel() - self.wallet = mock.Mock() - self.wallet.hotkey.ss58_address = "dummy_hotkey_address" - self.endpoint_url = get_base_url(BUCKET_SECRETS["account_id"]) - self.bucket_name = BUCKET_SECRETS["bucket_name"].split("/")[-1] - self.access_key = BUCKET_SECRETS["write"]["access_key_id"] - self.secret_key = BUCKET_SECRETS["write"]["secret_access_key"] - self.original_bucket_secrets = BUCKET_SECRETS.copy() - - def tearDown(self): - self.temp_dir.cleanup() - for key in self.original_bucket_secrets: - BUCKET_SECRETS[key] = self.original_bucket_secrets[key] - - async def test_async_behavior(self): - # Example test that ensures `save_and_upload` doesn't block - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - self.assertTrue(os.path.exists(checkpoint_manager.checkpoint_path)) - checkpoint_manager.cleanup() - - async def test_checkpoint_cleanup(self): - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - - # Save multiple checkpoints - for i in range(5): - await checkpoint_manager.save_and_upload( - global_step=i, block_number=100 + i - ) - - # Wait a moment for async tasks to finish - await asyncio.sleep(5) - - # Check that only the latest 3 checkpoints remain locally - pattern = os.path.join( - checkpoint_manager.checkpoint_dir, - f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b*_v{__version__}.pth", - ) - files = glob.glob(pattern) - self.assertEqual(len(files), 3) - checkpoint_manager.cleanup() - - async def test_checkpoint_local_save(self): - # If you previously tested `save_checkpoint` directly, now test `save_and_upload` - # to ensure a checkpoint is saved locally without error. - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - self.assertTrue(os.path.exists(checkpoint_manager.checkpoint_path)) - checkpoint_manager.cleanup() - - async def test_checkpoint_s3_upload(self): - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - - # Wait for the upload to complete - if checkpoint_manager.upload_task: - await checkpoint_manager.upload_task - - filename = os.path.basename(checkpoint_manager.checkpoint_path) - s3_exists = await upload_exists_in_s3( - self.bucket_name, - filename, - self.access_key, - self.secret_key, - self.endpoint_url, - ) - self.assertTrue(s3_exists) - - # Cleanup - await async_delete_from_s3( - self.bucket_name, - filename, - self.access_key, - self.secret_key, - self.endpoint_url, - ) - checkpoint_manager.cleanup() - - async def test_checkpoint_upload_with_invalid_credentials(self): - faulty_access_key = "invalid_access_key" - faulty_secret_key = "invalid_secret_key" - - with mock.patch.dict( - "templar.config.BUCKET_SECRETS", - { - "bucket_name": BUCKET_SECRETS["bucket_name"], - "account_id": BUCKET_SECRETS["account_id"], - "write": { - "access_key_id": faulty_access_key, - "secret_access_key": faulty_secret_key, - }, - }, - clear=False, - ): - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - - # Check if NOT uploaded with correct creds - filename = os.path.basename(checkpoint_manager.checkpoint_path) - s3_exists = await upload_exists_in_s3( - self.bucket_name, - filename, - self.access_key, - self.secret_key, - self.endpoint_url, - ) - self.assertFalse(s3_exists) - checkpoint_manager.cleanup() - - async def test_invalid_checkpoint_path(self): - # Test what happens if the checkpoint directory is invalid - invalid_path = "/invalid_dir/checkpoint.pth" - - with self.assertRaises(PermissionError): - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=invalid_path, - wallet=self.wallet, - device="cpu", - ) - # No need to proceed if initialization fails - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - - # No cleanup needed since initialization failed diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..664e8f0 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2629 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "sys_platform != 'linux'", + "sys_platform == 'linux'", +] + +[[package]] +name = "aioboto3" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore", extra = ["boto3"] }, + { name = "aiofiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/1d/a36f39e95d15202236a5fec436377a9db712c5fe5a240325a5e54bc5e3ef/aioboto3-13.2.0.tar.gz", hash = "sha256:92c3232e0bf7dcb5d921cd1eb8c5e0b856c3985f7c1cd32ab3cd51adc5c9b5da", size = 32497 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/66/e4b2d8f3d11687f7c63b1b63e484ee879f9af637b3564026037655d83255/aioboto3-13.2.0-py3-none-any.whl", hash = "sha256:fd894b8d319934dfd75285b58da35560670e57182d0148c54a3d4ee5da730c78", size = 34738 }, +] + +[[package]] +name = "aiobotocore" +version = "2.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/3d/5d54985abed848a4d4dafd10d7eb9ecd6bd7fff9533223911a92c2e6e15d/aiobotocore-2.15.2.tar.gz", hash = "sha256:9ac1cfcaccccc80602968174aa032bf978abe36bd4e55e6781d6500909af1375", size = 107035 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/57/6402242dde160d9ef9903487b4277443dc3da04615f6c4d3b48564a8ab57/aiobotocore-2.15.2-py3-none-any.whl", hash = "sha256:d4d3128b4b558e2b4c369bfa963b022d7e87303adb82eec623cec8aa77ae578a", size = 77400 }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/55/e4373e888fdacb15563ef6fa9fa8c8252476ea071e96fb46defac9f18bf2/aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", size = 21977 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/74/fbb6559de3607b3300b9be3cc64e97548d55678e44623db17820dbd20002/aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8", size = 14756 }, +] + +[[package]] +name = "aiohttp" +version = "3.10.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/a8/8e2ba36c6e3278d62e0c88aa42bb92ddbef092ac363b390dab4421da5cf5/aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7", size = 7551886 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/96/221ec59bc38395a6c205cbe8bf72c114ce92694b58abc8c3c6b7250efa7f/aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07", size = 587742 }, + { url = "https://files.pythonhosted.org/packages/24/17/4e606c969b19de5c31a09b946bd4c37e30c5288ca91d4790aa915518846e/aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695", size = 400357 }, + { url = "https://files.pythonhosted.org/packages/a2/e5/433f59b87ba69736e446824710dd7f26fcd05b24c6647cb1e76554ea5d02/aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24", size = 392099 }, + { url = "https://files.pythonhosted.org/packages/d2/a3/3be340f5063970bb9e47f065ee8151edab639d9c2dce0d9605a325ab035d/aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382", size = 1300367 }, + { url = "https://files.pythonhosted.org/packages/ba/7d/a3043918466cbee9429792ebe795f92f70eeb40aee4ccbca14c38ee8fa4d/aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa", size = 1339448 }, + { url = "https://files.pythonhosted.org/packages/2c/60/192b378bd9d1ae67716b71ae63c3e97c48b134aad7675915a10853a0b7de/aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625", size = 1374875 }, + { url = "https://files.pythonhosted.org/packages/e0/d7/cd58bd17f5277d9cc32ecdbb0481ca02c52fc066412de413aa01268dc9b4/aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9", size = 1285626 }, + { url = "https://files.pythonhosted.org/packages/bb/b2/da4953643b7dcdcd29cc99f98209f3653bf02023d95ce8a8fd57ffba0f15/aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac", size = 1246120 }, + { url = "https://files.pythonhosted.org/packages/6c/22/1217b3c773055f0cb172e3b7108274a74c0fe9900c716362727303931cbb/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a", size = 1265177 }, + { url = "https://files.pythonhosted.org/packages/63/5e/3827ad7e61544ed1e73e4fdea7bb87ea35ac59a362d7eb301feb5e859780/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b", size = 1257238 }, + { url = "https://files.pythonhosted.org/packages/53/31/951f78751d403da6086b662760e6e8b08201b0dcf5357969f48261b4d0e1/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16", size = 1315944 }, + { url = "https://files.pythonhosted.org/packages/0d/79/06ef7a2a69880649261818b135b245de5a4e89fed5a6987c8645428563fc/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730", size = 1332065 }, + { url = "https://files.pythonhosted.org/packages/10/39/a273857c2d0bbf2152a4201fbf776931c2dac74aa399c6683ed4c286d1d1/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8", size = 1291882 }, + { url = "https://files.pythonhosted.org/packages/49/39/7aa387f88403febc96e0494101763afaa14d342109329a01b413b2bac075/aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9", size = 363409 }, + { url = "https://files.pythonhosted.org/packages/6f/e9/8eb3dc095ce48499d867ad461d02f1491686b79ad92e4fad4df582f6be7b/aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f", size = 382644 }, + { url = "https://files.pythonhosted.org/packages/01/16/077057ef3bd684dbf9a8273a5299e182a8d07b4b252503712ff8b5364fd1/aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710", size = 584830 }, + { url = "https://files.pythonhosted.org/packages/2c/cf/348b93deb9597c61a51b6682e81f7c7d79290249e886022ef0705d858d90/aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d", size = 397090 }, + { url = "https://files.pythonhosted.org/packages/70/bf/903df5cd739dfaf5b827b3d8c9d68ff4fcea16a0ca1aeb948c9da30f56c8/aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97", size = 392361 }, + { url = "https://files.pythonhosted.org/packages/fb/97/e4792675448a2ac5bd56f377a095233b805dd1315235c940c8ba5624e3cb/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725", size = 1309839 }, + { url = "https://files.pythonhosted.org/packages/96/d0/ba19b1260da6fbbda4d5b1550d8a53ba3518868f2c143d672aedfdbc6172/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636", size = 1348116 }, + { url = "https://files.pythonhosted.org/packages/b3/b9/15100ee7113a2638bfdc91aecc54641609a92a7ce4fe533ebeaa8d43ff93/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385", size = 1391402 }, + { url = "https://files.pythonhosted.org/packages/c5/36/831522618ac0dcd0b28f327afd18df7fb6bbf3eaf302f912a40e87714846/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087", size = 1304239 }, + { url = "https://files.pythonhosted.org/packages/60/9f/b7230d0c48b076500ae57adb717aa0656432acd3d8febb1183dedfaa4e75/aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f", size = 1256565 }, + { url = "https://files.pythonhosted.org/packages/63/c2/35c7b4699f4830b3b0a5c3d5619df16dca8052ae8b488e66065902d559f6/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03", size = 1269285 }, + { url = "https://files.pythonhosted.org/packages/51/48/bc20ea753909bdeb09f9065260aefa7453e3a57f6a51f56f5216adc1a5e7/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d", size = 1276716 }, + { url = "https://files.pythonhosted.org/packages/0c/7b/a8708616b3810f55ead66f8e189afa9474795760473aea734bbea536cd64/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a", size = 1315023 }, + { url = "https://files.pythonhosted.org/packages/2a/d6/dfe9134a921e05b01661a127a37b7d157db93428905450e32f9898eef27d/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e", size = 1342735 }, + { url = "https://files.pythonhosted.org/packages/ca/1a/3bd7f18e3909eabd57e5d17ecdbf5ea4c5828d91341e3676a07de7c76312/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4", size = 1302618 }, + { url = "https://files.pythonhosted.org/packages/cf/51/d063133781cda48cfdd1e11fc8ef45ab3912b446feba41556385b3ae5087/aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb", size = 360497 }, + { url = "https://files.pythonhosted.org/packages/55/4e/f29def9ed39826fe8f85955f2e42fe5cc0cbe3ebb53c97087f225368702e/aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27", size = 380577 }, + { url = "https://files.pythonhosted.org/packages/1f/63/654c185dfe3cf5d4a0d35b6ee49ee6ca91922c694eaa90732e1ba4b40ef1/aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127", size = 577381 }, + { url = "https://files.pythonhosted.org/packages/4e/c4/ee9c350acb202ba2eb0c44b0f84376b05477e870444192a9f70e06844c28/aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413", size = 393289 }, + { url = "https://files.pythonhosted.org/packages/3d/7c/30d161a7e3b208cef1b922eacf2bbb8578b7e5a62266a6a2245a1dd044dc/aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461", size = 388859 }, + { url = "https://files.pythonhosted.org/packages/79/10/8d050e04be447d3d39e5a4a910fa289d930120cebe1b893096bd3ee29063/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288", size = 1280983 }, + { url = "https://files.pythonhosted.org/packages/31/b3/977eca40afe643dcfa6b8d8bb9a93f4cba1d8ed1ead22c92056b08855c7a/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067", size = 1317132 }, + { url = "https://files.pythonhosted.org/packages/1a/43/b5ee8e697ed0f96a2b3d80b3058fa7590cda508e9cd256274246ba1cf37a/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e", size = 1362630 }, + { url = "https://files.pythonhosted.org/packages/28/20/3ae8e993b2990fa722987222dea74d6bac9331e2f530d086f309b4aa8847/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1", size = 1276865 }, + { url = "https://files.pythonhosted.org/packages/02/08/1afb0ab7dcff63333b683e998e751aa2547d1ff897b577d2244b00e6fe38/aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006", size = 1230448 }, + { url = "https://files.pythonhosted.org/packages/c6/fd/ccd0ff842c62128d164ec09e3dd810208a84d79cd402358a3038ae91f3e9/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f", size = 1244626 }, + { url = "https://files.pythonhosted.org/packages/9f/75/30e9537ab41ed7cb062338d8df7c4afb0a715b3551cd69fc4ea61cfa5a95/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6", size = 1243608 }, + { url = "https://files.pythonhosted.org/packages/c2/e0/3e7a62d99b9080793affddc12a82b11c9bc1312916ad849700d2bddf9786/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31", size = 1286158 }, + { url = "https://files.pythonhosted.org/packages/71/b8/df67886802e71e976996ed9324eb7dc379e53a7d972314e9c7fe3f6ac6bc/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d", size = 1313636 }, + { url = "https://files.pythonhosted.org/packages/3c/3b/aea9c3e70ff4e030f46902df28b4cdf486695f4d78fd9c6698827e2bafab/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00", size = 1273772 }, + { url = "https://files.pythonhosted.org/packages/e9/9e/4b4c5705270d1c4ee146516ad288af720798d957ba46504aaf99b86e85d9/aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71", size = 358679 }, + { url = "https://files.pythonhosted.org/packages/28/1d/18ef37549901db94717d4389eb7be807acbfbdeab48a73ff2993fc909118/aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e", size = 378073 }, +] + +[[package]] +name = "aioitertools" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/de/38491a84ab323b47c7f86e94d2830e748780525f7a10c8600b67ead7e9ea/aioitertools-0.12.0.tar.gz", hash = "sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b", size = 19369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/13/58b70a580de00893223d61de8fea167877a3aed97d4a5e1405c9159ef925/aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796", size = 24345 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, +] + +[[package]] +name = "async-property" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/12/900eb34b3af75c11b69d6b78b74ec0fd1ba489376eceb3785f787d1a0a1d/async_property-0.2.2.tar.gz", hash = "sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380", size = 16523 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl", hash = "sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7", size = 9546 }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, +] + +[[package]] +name = "base58" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621 }, +] + +[[package]] +name = "bittensor" +version = "8.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "async-property" }, + { name = "bittensor-cli" }, + { name = "bittensor-commit-reveal" }, + { name = "bittensor-wallet" }, + { name = "bt-decode" }, + { name = "colorama" }, + { name = "fastapi" }, + { name = "msgpack-numpy-opentensor" }, + { name = "munch" }, + { name = "nest-asyncio" }, + { name = "netaddr" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pycryptodome" }, + { name = "pydantic" }, + { name = "python-levenshtein" }, + { name = "python-statemachine" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "retry" }, + { name = "rich" }, + { name = "scalecodec" }, + { name = "setuptools" }, + { name = "substrate-interface" }, + { name = "uvicorn" }, + { name = "websockets" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/b2/09ec0664822d32fcf2f708742c69249f8a70a09fb941cef3a1f4ab8eca02/bittensor-8.5.1.tar.gz", hash = "sha256:f1bb033ba1e2641881d37f9d8cfebdcb7145ae20975861863710bdd17941cce4", size = 210235 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/34/29f5b570d734b8474b21071756db41b4717c8cbebc3fa5716f0c499a12e8/bittensor-8.5.1-py3-none-any.whl", hash = "sha256:8dbf9c389d10fd043dab5da163377a43ec2ae1b1715e819a3602e07d36304f94", size = 257860 }, +] + +[[package]] +name = "bittensor-cli" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "async-property" }, + { name = "backoff" }, + { name = "bittensor-wallet" }, + { name = "bt-decode" }, + { name = "fuzzywuzzy" }, + { name = "gitpython" }, + { name = "jinja2" }, + { name = "netaddr" }, + { name = "numpy" }, + { name = "pycryptodome" }, + { name = "pytest" }, + { name = "python-levenshtein" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "scalecodec" }, + { name = "substrate-interface" }, + { name = "typer" }, + { name = "websockets" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/77/a4250b215d7a5dabef2090c8b300843aa962a135a85b8af0ca499b15a23e/bittensor-cli-8.4.2.tar.gz", hash = "sha256:43efc081ed2ecf4357bf5c5322ccd6f7d1a5110eb842cf138c75adb3f21686fd", size = 161405 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/7b/3f43e1e453697aba95734f94a44aeb96d72139a2ca83837ecaf170a58de2/bittensor_cli-8.4.2-py3-none-any.whl", hash = "sha256:e7fc5ff510f039fa0cb9c0c701a56c4eb2b644befb019b1cd0fac29546bfb764", size = 171700 }, +] + +[[package]] +name = "bittensor-commit-reveal" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/93/f6361d6d617f1620f1b642308384d7f22c7917c169b821ddb3a90856a0c9/bittensor_commit_reveal-0.1.0.tar.gz", hash = "sha256:1c8bb8d77f6279988902c5c28361cc460167829c63ffa8d788209f8810933211", size = 23249 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/cf/fcc202fb07594933f759287ceea9e891cbb8ce779f24cc84311af2b50802/bittensor_commit_reveal-0.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2bb23935ac60a981bfb3d83397b83e858c0b69a11806969cf56486f5ebc90943", size = 493021 }, + { url = "https://files.pythonhosted.org/packages/c6/bd/0e438e505036fda9370f352dc9a8f1ff7fa777a8b07479b9874f5742e7b4/bittensor_commit_reveal-0.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4917b215c24b10bd80c84db921113b9cd1346ca7dcaca75e286905ede81a3b18", size = 493236 }, + { url = "https://files.pythonhosted.org/packages/2b/7a/cded935634bf0a077e8f7454b47164e1b3e45064234eaf9722e6a35c1cbf/bittensor_commit_reveal-0.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c46cee3e5fa5fc9e6f6a444793062855f40495c1a00b52df6508e4449ac5e89f", size = 711674 }, + { url = "https://files.pythonhosted.org/packages/06/ab/ea0f20581a786ec4b497bdaab8fb4a046c81d125820fc1ec4bfe79854f96/bittensor_commit_reveal-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56407b879dcf82bdde5eaefede43c8891e122fefc03a32c77a063dfc52e0c8", size = 552162 }, + { url = "https://files.pythonhosted.org/packages/a8/3a/7705ea18c3d61c8affc4696b8ab483bdb7e3d0bfdfb61ca1583a787ef1e0/bittensor_commit_reveal-0.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8509250549b6f5c475a9150e941b28fc66e82f30b27fe078fd80fa840943bb7b", size = 491259 }, + { url = "https://files.pythonhosted.org/packages/80/21/02b400750c7d1d5ed081dc22c740e21e22fd72fbb18b72517d5687eca8bd/bittensor_commit_reveal-0.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bed04f82f162121747cfd7f51bb5d625dda0bf763a0699054565f255d219a9c2", size = 492612 }, + { url = "https://files.pythonhosted.org/packages/9a/82/bf02fda4c7bfbe6830709476cf1893ad4e7b591c4e1f62eab2abbfcd0106/bittensor_commit_reveal-0.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af2d9c82cacc4278095460493430d36070cb2843c0aa54b1c563788d0742eb", size = 712159 }, + { url = "https://files.pythonhosted.org/packages/31/d1/7e41e52251c277bf0bebe0fcb3f700e6faf6a488c9cefa8b8fb2bae42cee/bittensor_commit_reveal-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8f530793274698aaf4ac7cc8f24e915749d8156df8302c9e1e16446177b429d", size = 551180 }, + { url = "https://files.pythonhosted.org/packages/fa/20/272b35206c52db8b385ff7f2a6579ca700fa996c147e4533cd4d323446a7/bittensor_commit_reveal-0.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e52955d652b61f091310e2b9b232d26b9e586a928e3b414a09a1b6615e9cc7a0", size = 491231 }, + { url = "https://files.pythonhosted.org/packages/ee/05/02329c66db0970569a31779c0effcee67a1f6bb20a12ccbd667123d89f3f/bittensor_commit_reveal-0.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7be8c8f79dea2e137f5add6ee4711447c4f5d43668be26616ab7c1cacf317e07", size = 492469 }, + { url = "https://files.pythonhosted.org/packages/f4/47/ca9a347273e6993b8775a2a04e9d3df5569aaab46dc95247bf0c1f1b5ea1/bittensor_commit_reveal-0.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88ecb6a0989c2200486e29419a8e7d3b3f7918bdbde4ec04dbb4464abdee08f", size = 711920 }, + { url = "https://files.pythonhosted.org/packages/fe/87/cbef0fa4b4d3159030d61d09da5a09181c0ca8f25bbb451437cb50627ac7/bittensor_commit_reveal-0.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac015f9eefa9dbddd2875cd7214e3a0bc2e394a2915772e655bdcc5c0af67de", size = 551137 }, +] + +[[package]] +name = "bittensor-wallet" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "eth-utils" }, + { name = "munch" }, + { name = "password-strength" }, + { name = "py-bip39-bindings" }, + { name = "rich" }, + { name = "substrate-interface" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/17/38a9ec85be2167dd2c1aa2e75f0ac7c25ccf7c31859fe9b0d325b474fbbb/bittensor_wallet-2.1.3.tar.gz", hash = "sha256:41927d7e5d68fff1494cef5abd861ede0afc684dff366824b0806cfa3ce13af0", size = 70285 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/5d/3b4a4ed5e4d4bbc3575001455dfd5631620147e65ab07f3f3a31891ea56a/bittensor_wallet-2.1.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a5199c84e9d33ccec451294f89d9354b61568a0b623ceee995f588ccdc14ea5c", size = 800061 }, + { url = "https://files.pythonhosted.org/packages/97/7c/8f55e5dfda6c28a74a63ca60cd4d9e860bb798da5e58ea4b88eead124f38/bittensor_wallet-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a34e524f21e8c7bd8edfd54db530480b81f48d2334a0a11b86ea22d9e349137c", size = 752208 }, + { url = "https://files.pythonhosted.org/packages/07/5b/bf271ddda747244ff044d8f7e21e30ff684f24d0a5447662cc020c3c301c/bittensor_wallet-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45a1556e02304e1e8e91059cc11bb8346fa2334ac039f79bb1e6f630fa26657f", size = 3146730 }, + { url = "https://files.pythonhosted.org/packages/c2/97/a74c138b92db1d455d2be371cea3777616fc6cb94ac401cecddd27e4d9d4/bittensor_wallet-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9399c753c37dbe63430c5aff4fba0a038e0349dde0061d623506a24e3b4d2cec", size = 2953376 }, + { url = "https://files.pythonhosted.org/packages/a7/b0/a803fb7abe4b004464d67f6812f5067ee0346e7ba0bfb1e3012f569261cd/bittensor_wallet-2.1.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e2f0d03a21a0c54b1f8cd59f34941d7a60df490e9aab7d7776b03f290de6074", size = 797657 }, + { url = "https://files.pythonhosted.org/packages/24/35/506d88aed623872fe4ecbcc2d6484ac864dc2c639ef8810141628fd28763/bittensor_wallet-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24c446b0af4c9ffc3ac122f97a1de25b283c877aa49892748ad06a8a61a74e13", size = 752425 }, + { url = "https://files.pythonhosted.org/packages/eb/37/c6feb7d6ac75c24bfe170ffabbd42f2d91bc34cc75b99575f2417ec486b1/bittensor_wallet-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eafd9c82720644b3eeac2f68deaa9cec4cf175836b16c89206d98ce22590e8e", size = 3146851 }, + { url = "https://files.pythonhosted.org/packages/8e/63/0dfe52c8c4c7d943d3ca2f52530039e1ee0dbdbffb3d16a90d770725b9bd/bittensor_wallet-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5122b05d8eede2bfc2eb242214b75ecab08f0da5d4f7547ed01ad253349e019", size = 2954118 }, + { url = "https://files.pythonhosted.org/packages/ad/81/670424362f512f96760694839cd44a1d4aa6401d5e1c93ff1bf37f3a3653/bittensor_wallet-2.1.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:88020b18aa2f91b336a6f04354b7acb124701f9678d74e41f5ffb64a7e1e5731", size = 797707 }, + { url = "https://files.pythonhosted.org/packages/e8/de/81744fd99af5339aa196c4c5e559ae3d2dd773d8fc1e39059fd651982b4b/bittensor_wallet-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7dd2ed4c12e617574b7302a6c20fb8e915477ce2942627f624293b5de9a003", size = 752028 }, + { url = "https://files.pythonhosted.org/packages/41/3c/309505722c2390337d417c17cc50040ddcbdaee03cc8fc664a34320f777a/bittensor_wallet-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de47dea7d283e83449465f9780d4dde608fe09da45d6ef8c795806e49ccf4fd2", size = 3145919 }, + { url = "https://files.pythonhosted.org/packages/bc/3f/e973420941b0d0b23d944fd60cd95c3bbbca38f5c582d83409f6243880fa/bittensor_wallet-2.1.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e35adc5303b2186df889e07c79bf0bc074df382df49e6c216a8feb27f00453a4", size = 2953541 }, +] + +[[package]] +name = "boto3" +version = "1.35.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/9f/17536f9a1ab4c6ee454c782f27c9f0160558f70502fc55da62e456c47229/boto3-1.35.36.tar.gz", hash = "sha256:586524b623e4fbbebe28b604c6205eb12f263cc4746bccb011562d07e217a4cb", size = 110987 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/6b/8b126c2e1c07fae33185544ea974de67027afc905bd072feef9fbbd38d3d/boto3-1.35.36-py3-none-any.whl", hash = "sha256:33735b9449cd2ef176531ba2cb2265c904a91244440b0e161a17da9d24a1e6d1", size = 139143 }, +] + +[[package]] +name = "botocore" +version = "1.35.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/4f/11d2d314f0bdbe7ff975737d125e1a5357115afe28fcc64f13e68b05ba61/botocore-1.35.36.tar.gz", hash = "sha256:354ec1b766f0029b5d6ff0c45d1a0f9e5007b7d2f3ec89bcdd755b208c5bc797", size = 12808757 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/60/056d58b606731f94fe395266c604ea9efcecc10e6857ceb9b10e6831d746/botocore-1.35.36-py3-none-any.whl", hash = "sha256:64241c778bf2dc863d93abab159e14024d97a926a5715056ef6411418cb9ead3", size = 12597046 }, +] + +[[package]] +name = "bt-decode" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/a9/7449c1073af4ef57520fc01e587a664591ff0331b694a3ec9c1aff3c3133/bt_decode-0.4.0.tar.gz", hash = "sha256:5c7e6286a4f8b9b704f6a0c263ce0e8854fb95d94da5dff6e8835be6de04d508", size = 3496621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/53/43502e90c428e0ff4946112349a6072a52b3c0e73f770284f1c530f5ad53/bt_decode-0.4.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e2dd446b5956c3c772cdcbfe08fe0d483e68dc07b1606cde5d39c689dffd736c", size = 561621 }, + { url = "https://files.pythonhosted.org/packages/64/f2/a869f4d3bf750a2247a10028b7523e12ba9c62fad072fc88741e64d42236/bt_decode-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcbb0fb758460c5fe7e5276b4406dd15d22ff544d309dd4ebb8fc998ce30d51f", size = 547050 }, + { url = "https://files.pythonhosted.org/packages/b8/c0/d6295ccf4c83dc4b10a19c54a116939a0935350b182d55abf86a36cae7aa/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816f45a75dc78d6beafaf7cc02ab51d73a3dd1c91d4ba0e6b43aae3c637d793d", size = 603391 }, + { url = "https://files.pythonhosted.org/packages/e9/c0/457f63f087b0072e877582e61fac115218b28902df5d9c62d60a42c899d5/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:39d44102ea27a23644c262d98378ac0ac650e481508f5d6989b8b4e3fd638faf", size = 600597 }, + { url = "https://files.pythonhosted.org/packages/3b/2d/e90271fa86038fcace7eb544923422d91ae36ebf8627291c84ec05d9d22c/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82e959521c60bc48276a91a01bd97726820128a4f4670ae043da35ca11823ca3", size = 669588 }, + { url = "https://files.pythonhosted.org/packages/c0/3e/5d8be99d4d1b3193f526ba12e64fb8c0132511c19859def040f19cdcd2d5/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdea70a4b83e46432999f7743d130dbd49ccf1974c87c87153f7ad3733f5ccea", size = 707978 }, + { url = "https://files.pythonhosted.org/packages/0e/de/2757cab0397594e8547c897696c0983d067c758b1d3ad9cfb944e401bde2/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99b6cc694fe05037c1dca02111d25b2357fd460bea8d8ce9b2432e3ed1d049c", size = 613663 }, + { url = "https://files.pythonhosted.org/packages/7c/15/c0d12ac696b7472f63bb32c61b4b94d75298311840ba315a76b9e2c9a5aa/bt_decode-0.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:645e82838b2e8d7b03686f5cee44e880c56bed3a9dbf2a530c818d1a63544967", size = 664223 }, + { url = "https://files.pythonhosted.org/packages/28/99/c6199f74f1f36279ced846c32d03f245b0d4d8fd2ae1b22842f6cfc4623d/bt_decode-0.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cb32f5c5fda6cada107e3d82b5d760c87cd49075f28105de0900e495ee211659", size = 781056 }, + { url = "https://files.pythonhosted.org/packages/a1/77/896b5f76f4b10d637ffdfd5645f739f5037ff7e7c871cb874528f2c02c40/bt_decode-0.4.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d2ecb71c8b40f3a4abd9c8fda54febffaa298eceafc12a47e9c0cf93e4ccbb8b", size = 861550 }, + { url = "https://files.pythonhosted.org/packages/5a/ea/d2f0b5c8bc2ac59676aa904b4af040f38730caec73fefd8547aabc4222ae/bt_decode-0.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9b7691207021f023485d5adff6758bc0f938f80cf7e1ca05d291189e869217b5", size = 819734 }, + { url = "https://files.pythonhosted.org/packages/dd/82/f7bd11e8b351d5c560daefe87b8884c6e735e1d3eabcd2919684395fb361/bt_decode-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912957e7373014acf4203f3a701f4b820d9d7f5bee1f710298d7346f12bcff59", size = 783927 }, + { url = "https://files.pythonhosted.org/packages/36/0c/0818b22b21ac168cfa07a9f7a46ca7676b175b1e65956dc5700d12c7f744/bt_decode-0.4.0-cp311-cp311-win32.whl", hash = "sha256:fb47926e13f39663e62b4105b436abc84b913cb27edd621308f441cb405956ac", size = 389847 }, + { url = "https://files.pythonhosted.org/packages/96/60/94e86a68062d69c42f3409a48143407a67c6c4cfbcd428ab46d10993fd0a/bt_decode-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:001995ff6a20438c5542b13ae0af6458845381ccfd0ef484ae5f7e012c6fb383", size = 416482 }, + { url = "https://files.pythonhosted.org/packages/29/08/090efa626ad7bb545febf8e47a96dd976effcf6c027ff06cf6e053d83104/bt_decode-0.4.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ee9731ecf76ba4f60e10378b16d15bea826b41183ab208e32a9a7fd86d3b7c21", size = 557364 }, + { url = "https://files.pythonhosted.org/packages/6c/53/7e32ff14583db56a9f1ecc2a506a4af9ca6106e2240928d937b0516e0934/bt_decode-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e0ebd9e6f6e710fce9432d448a6add5b266f19af5ec518a2faf19ddd19ce3dc", size = 542812 }, + { url = "https://files.pythonhosted.org/packages/30/39/835655b931dd4b7734743bf66caf28bd94cd5067a8141f6ce22bb8e2de91/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd898558c915dd9374a1860c1aee944cd6acb25f8e0f33f58d18eb989c49fab", size = 604124 }, + { url = "https://files.pythonhosted.org/packages/15/8d/0920fcfa46296fb23093d80554cc305d66a0e66d82b392aea8cd70004dc8/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f87500550b030c3d265ab6847ef25f1e4f756b455605f1977329a665e41b330", size = 600859 }, + { url = "https://files.pythonhosted.org/packages/6a/86/0a709fb430d157d0be29733a66e56ee78f8354b2dfba42a64feeb54d6e42/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59fa64d5eff9fcc00f536e3ef74932f40aeff1335bd75a469bce90c1762451ae", size = 669825 }, + { url = "https://files.pythonhosted.org/packages/d4/83/58495d791a8be3ee5064af3d6e4039f11a0b13dd3b30e8c91dc247405f23/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2be0732720588d047b00eb87e234dd83ebbdb717da8d704b8930b9ab580a6c3", size = 708326 }, + { url = "https://files.pythonhosted.org/packages/56/be/ac3f35a7c23929c428a705e872f596a86afc0eae76d3276b79872abb2817/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4107e8b75966c5be0822a5f0525b568c94dbc1faa8d928090fa48daa329b45", size = 614048 }, + { url = "https://files.pythonhosted.org/packages/7e/ee/6b16c47b5ac00cd511da91ab762c3d2353ba9983f205e8d47a77419221f5/bt_decode-0.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46e09e7c557fe753c20226ec4db887a4a1b520d36dc4d01eb5d2bd2e2846970e", size = 664008 }, + { url = "https://files.pythonhosted.org/packages/04/09/97f411183dd7497edcf5f0d6cbbd1ef56655395b18e614e272698a9d6802/bt_decode-0.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e817fe5e805bc393b266909709660dc14bd34a671712da0087e164a760b928b4", size = 781116 }, + { url = "https://files.pythonhosted.org/packages/71/f8/ec920e1713e24462142f55aa85c1ad6969d826e2cb32d583ccc37fa8ddb4/bt_decode-0.4.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:59f9a61789003c345b423f1728ee0d774f89cc41be0ab2af0f2ad6e2653084b5", size = 862290 }, + { url = "https://files.pythonhosted.org/packages/8b/c7/5b0504f14f1b8c9b60c69a080832f53774f30db181e472944260e0cfbf1c/bt_decode-0.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:905715452ecf4ce204aa937ee8266ea539fc085377f92bd9506ec76dcd874347", size = 819695 }, + { url = "https://files.pythonhosted.org/packages/13/9e/5d2953e4416db004d21f6c480657c8f9b84ee27b48fe5478d2cdba2ec49a/bt_decode-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e85f5f12e6bb00253e194372d90e60f129d613f0ddedae659d3b9a3049a69cf", size = 784116 }, + { url = "https://files.pythonhosted.org/packages/7e/b2/26f374ee94c88a90310569bd5d2f282c105a7ee1ae298e0282d3ee560f50/bt_decode-0.4.0-cp312-cp312-win32.whl", hash = "sha256:ed4c3c4383c9903f371502c0d62ce88ecd2c531044e04deaeb60c827ae45ad8e", size = 390937 }, + { url = "https://files.pythonhosted.org/packages/7e/35/0610ddaf739013a3fff13961edadeefff4be83fff7735bc0592214f0246b/bt_decode-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:68beccbb00f129b75d189d2ffc48fd430bf4eab8a456aab79615b17eec82437d", size = 417431 }, + { url = "https://files.pythonhosted.org/packages/6b/2f/4cdfdf8bd52a38e27b50f36e9b9288085a9bab1d703310cc426e4b4243be/bt_decode-0.4.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:88de7129c3323c36cd6cce28844fb475556a865ec6fc87934ec5deeb95ff2d86", size = 557018 }, + { url = "https://files.pythonhosted.org/packages/43/16/7d29d9f719bab8f3890d6d6dfaaade16aa7616e57bdde8f0114781430134/bt_decode-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:056e6245a2119b391306542134651df54df29569136be892411073fc10840c8e", size = 542668 }, + { url = "https://files.pythonhosted.org/packages/c5/d3/a15421174b9943fd86f2470bfe109b6b6a800a2e9cca414b5bb1b2367752/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:faa76d0b8fcb0f9ae2107e8c6ae84ea670de81c0adda4967a52d4b7d1de8c605", size = 603689 }, + { url = "https://files.pythonhosted.org/packages/9e/e7/ef333c2c6c2b2319fef3e28ef9d5a2e82c30b8c7f7f3875b182dae7fc957/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a3ff15bfe86d482e642dfaa6e5581b65815e7663f337af7502b422fea2fdcc2", size = 600436 }, + { url = "https://files.pythonhosted.org/packages/7c/4c/3bd5c96dcf2ef09d73f0d35cbdc0d32c1b8f9f0c0d9e10af087405f38e7d/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa7687c01c516f84274a2e71ba717898eef095e08ec7125823f7a4e230bd46fe", size = 669460 }, + { url = "https://files.pythonhosted.org/packages/81/d7/df22e559dfe7941edfb33357fbc2dc9f6025ae4fb58740213dc09b1dd53b/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d3cf8cfff714600db01c6cd144906fe0a8be85293711e279b8089f6ccaffd71", size = 707396 }, + { url = "https://files.pythonhosted.org/packages/2c/19/0d1eeb47ac8844021e6f7f69c92069c0c80ccee1de1614a9e5dac96da50e/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:983972ecc83bd0507e72ae316281960b7e26e31386525c7905f7cdb8fa3e7de1", size = 613845 }, + { url = "https://files.pythonhosted.org/packages/a2/06/308512e5f17e3b3a9472d2271114da0caa394c38523b7d0aa5fc75ee3b89/bt_decode-0.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32e3950b120b8b59ae5ab70005ba9b5c7560a0e222e805f47878cb259a32ed39", size = 663927 }, + { url = "https://files.pythonhosted.org/packages/a4/53/ec4fc237ffe8b8f7e8e4bd78b54b0c82abad5407f3faed7df0828ba2f0f2/bt_decode-0.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66d906ac225e3cd169dde1e0af21e8d73e8ea7dea3f7e9afcdec501bced3d83a", size = 781071 }, + { url = "https://files.pythonhosted.org/packages/d2/d7/700ddb1280e5aafd0404f445847ec6c4c27f7df949a7d148e8dc3c0f5a3f/bt_decode-0.4.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:58bf09b004dc182748e285b5bc15ac6305af4ab9c318f995c443ba33bb61fbb6", size = 862093 }, + { url = "https://files.pythonhosted.org/packages/53/32/c9d9a5787f793da0ac8a9b5c950f45ad8b2449a751cf5b84ab430c2bc9f7/bt_decode-0.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c202f22152b3186cbc1c319250d6b0ecfe87cf9a4e8e90b19cc9f83786acdf1a", size = 819486 }, + { url = "https://files.pythonhosted.org/packages/3f/94/c182bd002357d68d663a118dc41b95d5f400aac6e9e5074c53693b6de41a/bt_decode-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6dd31b0947b7b15a36f7f9bfdb8ae30ffe3f3f97e0dc4d60bf79b9baf57f4e5", size = 784067 }, + { url = "https://files.pythonhosted.org/packages/29/9c/a17e71aa0e4f674c7a59b5e65b042d2bdf91bebc316e969a1c31c6b51ef1/bt_decode-0.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebb3b72146e7feb08e235d78457b597697708149d7410f184098b73c5ab38aa", size = 600955 }, + { url = "https://files.pythonhosted.org/packages/f4/c6/429323a3c72251c6bc22926995ea3e490db07bb96e608ac4ca9eaa282e62/bt_decode-0.4.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9571680e6b74fab00cbd10dc255594692a9cdf615e33170d5a32112c1da8e3e4", size = 599227 }, + { url = "https://files.pythonhosted.org/packages/15/f4/3495a7d242668d347e851424e95acbbd2916ae70f7827e0533bd3c59e653/bt_decode-0.4.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dec8af1719ced86da6f7b1dcf70e1d480cfb86e2cf7530692d3e66ad1e16067d", size = 666872 }, + { url = "https://files.pythonhosted.org/packages/f1/3a/f0875014848888259f8646f915c1a8046d420799a155ce80d5af10e77044/bt_decode-0.4.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46d2308e13615951f89ff7ba05364a2e3747626b29fd4ee39c085ea56cb5fe", size = 709410 }, + { url = "https://files.pythonhosted.org/packages/be/e5/bc31c0f2a29945c548cda2538c8b5368da722217da7ca0a64eedd4df56a2/bt_decode-0.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0df0436d736544587002e0fa4fe3887b28cec8de4a9036c1ea776c560e966b8d", size = 778135 }, + { url = "https://files.pythonhosted.org/packages/25/48/387fd8cef96a86c39e6716455b493a759fbe9a67bcaa2dfe39c3d3b6b11b/bt_decode-0.4.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:579aba5010a078831af2025cd03df9d429fa35008ec46bc1561e6147e2c9769e", size = 860601 }, + { url = "https://files.pythonhosted.org/packages/12/85/1458d9eaf9a74390ac5e0a1a3be5eaf53550aa4f4c28362fb4f80a94c8a6/bt_decode-0.4.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:039e880688d4c5f2ee090980649811b700593e21eccee520b294c07b85008bce", size = 817941 }, + { url = "https://files.pythonhosted.org/packages/70/72/723265284f71fb95556c5b27c83a370b2e38e02666fd17dbb129856fb1f2/bt_decode-0.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a45173a6f0e48b28b190bfb250b6683984d115d70a6d2ff5102a2421d581de6", size = 783857 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[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 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "43.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 }, + { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, + { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, + { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, + { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, + { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, + { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, + { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 }, + { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 }, + { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 }, + { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, + { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, + { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, + { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, + { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, + { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, + { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 }, + { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 }, +] + +[[package]] +name = "cytoolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/4c/ca9b05bdfa28ddbb4a5365c27021a1d4be61db7d8f6b4e5d4e76aa4ba3b7/cytoolz-1.0.0.tar.gz", hash = "sha256:eb453b30182152f9917a5189b7d99046b6ce90cdf8aeb0feff4b2683e600defd", size = 626708 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/99/b489081777ad02c9bba294c757583416d0bdbd9403017145aba68145c16f/cytoolz-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dffc22fd2c91be64dbdbc462d0786f8e8ac9a275cfa1869a1084d1867d4f67e0", size = 406148 }, + { url = "https://files.pythonhosted.org/packages/b2/d3/a4d58bf89924dbca34e8dbb643b26935e08c16b4a2ee255d43a8b7489939/cytoolz-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a99e7e29274e293f4ffe20e07f76c2ac753a78f1b40c1828dfc54b2981b2f6c4", size = 384956 }, + { url = "https://files.pythonhosted.org/packages/81/d4/4d09e6571ef3b143f668c590a7a00c97ff24e6df6901f457ea7c782cd2ba/cytoolz-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c507a3e0a45c41d66b43f96797290d75d1e7a8549aa03a4a6b8854fdf3f7b8d8", size = 2091688 }, + { url = "https://files.pythonhosted.org/packages/8d/7b/68c89bed2e0490e9b946574c3bc79711179f35b1dc5eb31046c535f1bed2/cytoolz-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:643a593ec272ef7429099e1182a22f64ec2696c00d295d2a5be390db1b7ff176", size = 2188448 }, + { url = "https://files.pythonhosted.org/packages/56/a3/4e536fc7b72fd7495e19180463e8160a4fe1d50ab59a5854fc596621d5c3/cytoolz-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ce38e2e42cbae30446190c59b92a8a9029e1806fd79eaf88f48b0fe33003893", size = 2174196 }, + { url = "https://files.pythonhosted.org/packages/5a/7f/0451778af9e22755a95ef4400ee7fc6e41387521ab0f17699593cb07169a/cytoolz-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810a6a168b8c5ecb412fbae3dd6f7ed6c6253a63caf4174ee9794ebd29b2224f", size = 2099823 }, + { url = "https://files.pythonhosted.org/packages/58/b7/8ffdef1ac8f74b0cc650b9d4a74d93d911a7e20fcf7cc0abac0f4bce225f/cytoolz-1.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ce8a2a85c0741c1b19b16e6782c4a5abc54c3caecda66793447112ab2fa9884", size = 1996733 }, + { url = "https://files.pythonhosted.org/packages/1c/ab/9c694c883f3038d167b797cc55c64c2bdb64146428000cea15f235f30a0f/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ea4ac72e6b830861035c4c7999af8e55813f57c6d1913a3d93cc4a6babc27bf7", size = 2013725 }, + { url = "https://files.pythonhosted.org/packages/6c/df/859faaee91c795dc969c79cd38159031f373828d44b0b18999feb7d9a44d/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a09cdfb21dfb38aa04df43e7546a41f673377eb5485da88ceb784e327ec7603b", size = 1994851 }, + { url = "https://files.pythonhosted.org/packages/34/2a/26ac5a34e859c5ba32351f5a74492f4ed12e7a7e75b6afccf11c4100aa70/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:658dd85deb375ff7af990a674e5c9058cef1c9d1f5dc89bc87b77be499348144", size = 2155343 }, + { url = "https://files.pythonhosted.org/packages/3d/41/f687d2e40407b29bfcce36a7d456dad368283ea543aa39da53bcc119974e/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9715d1ff5576919d10b68f17241375f6a1eec8961c25b78a83e6ef1487053f39", size = 2163507 }, + { url = "https://files.pythonhosted.org/packages/39/7c/70d529f909d97ea214d59923c19e3d05a3768fe8e2066542b72550a31ca4/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f370a1f1f1afc5c1c8cc5edc1cfe0ba444263a0772af7ce094be8e734f41769d", size = 2054428 }, + { url = "https://files.pythonhosted.org/packages/5a/4a/7bb2eafe4077f6d9867547ca74ca4b75bc8a081e32a47e186e5c067f6cab/cytoolz-1.0.0-cp311-cp311-win32.whl", hash = "sha256:dbb2ec1177dca700f3db2127e572da20de280c214fc587b2a11c717fc421af56", size = 322300 }, + { url = "https://files.pythonhosted.org/packages/5a/ed/a1c955444343224ab1317a41f6575bc640055eb2495e8f9175f8f28bd776/cytoolz-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:0983eee73df86e54bb4a79fcc4996aa8b8368fdbf43897f02f9c3bf39c4dc4fb", size = 365539 }, + { url = "https://files.pythonhosted.org/packages/28/2e/a8b71f74ee75f33164bfbc6324ddd1e8d0f425255b1c930141516f51d539/cytoolz-1.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10e3986066dc379e30e225b230754d9f5996aa8d84c2accc69c473c21d261e46", size = 414110 }, + { url = "https://files.pythonhosted.org/packages/c6/0a/999af6bb896375b0c687e292a3dcd4edb338a2764bbac40c0ce11eb21c64/cytoolz-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:16576f1bb143ee2cb9f719fcc4b845879fb121f9075c7c5e8a5ff4854bd02fc6", size = 390900 }, + { url = "https://files.pythonhosted.org/packages/30/04/02f0ee5339f8c6ef785f06caee85e17e8e0b406e7e553c8fd99a55ff8390/cytoolz-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3faa25a1840b984315e8b3ae517312375f4273ffc9a2f035f548b7f916884f37", size = 2090729 }, + { url = "https://files.pythonhosted.org/packages/04/de/296ded5f81ada90ae4db8c06cc34b142cf6c51fabb4c3c78583abba91c36/cytoolz-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:781fce70a277b20fd95dc66811d1a97bb07b611ceea9bda8b7dd3c6a4b05d59a", size = 2155926 }, + { url = "https://files.pythonhosted.org/packages/cc/ce/d5782bdd3d2fd16d87e83e70e14fcfa65ba67ba21cf7e1007505baef7d79/cytoolz-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a562c25338eb24d419d1e80a7ae12133844ce6fdeb4ab54459daf250088a1b2", size = 2171893 }, + { url = "https://files.pythonhosted.org/packages/d0/02/22a8c74ff13f8a08e8cacd0a0aa34da3a6e3637cf477e376efc61f7567e5/cytoolz-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f29d8330aaf070304f7cd5cb7e73e198753624eb0aec278557cccd460c699b5b", size = 2125265 }, + { url = "https://files.pythonhosted.org/packages/50/d1/a3f2e2ced1fa7e2b5607d05ed4de9951491004a4804e96f78778d11bebd4/cytoolz-1.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98a96c54aa55ed9c7cdb23c2f0df39a7b4ee518ac54888480b5bdb5ef69c7ef0", size = 1973962 }, + { url = "https://files.pythonhosted.org/packages/3e/10/174d9585e1011824e2e6e79380f8b1c6e49070c35a278d823d996d1c11e6/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:287d6d7f475882c2ddcbedf8da9a9b37d85b77690779a2d1cdceb5ae3998d52e", size = 2021691 }, + { url = "https://files.pythonhosted.org/packages/84/aa/bebdca3ae140698d3d4fe75ffd4c87a15ee999cee6b994e0831e5a24cdd7/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:05a871688df749b982839239fcd3f8ec3b3b4853775d575ff9cd335fa7c75035", size = 2010169 }, + { url = "https://files.pythonhosted.org/packages/2e/9f/8d5940c953534a4d2ae4419bb4fdc1eb5559345fed1f4838708073d7e6b4/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:28bb88e1e2f7d6d4b8e0890b06d292c568984d717de3e8381f2ca1dd12af6470", size = 2154314 }, + { url = "https://files.pythonhosted.org/packages/84/bf/a5414601c95afde30a0c038c5d6273c188f1da8ff25a057bd0c683679e5c/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:576a4f1fc73d8836b10458b583f915849da6e4f7914f4ecb623ad95c2508cad5", size = 2188368 }, + { url = "https://files.pythonhosted.org/packages/67/fe/990ea30d56b9b6602f3bf4af77a1bfd9233e6ffb761b11b8864619fed508/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:509ed3799c47e4ada14f63e41e8f540ac6e2dab97d5d7298934e6abb9d3830ec", size = 2077906 }, + { url = "https://files.pythonhosted.org/packages/ab/95/94b936501e1e23460732631e5d7c6efc4f6c09df21a594dfca9bf30b9411/cytoolz-1.0.0-cp312-cp312-win32.whl", hash = "sha256:9ce25f02b910630f6dc2540dd1e26c9326027ddde6c59f8cab07c56acc70714c", size = 322445 }, + { url = "https://files.pythonhosted.org/packages/be/04/a49b73591b132be5a28c0670229629a3c002cfac59582a1d38b16bdc6fed/cytoolz-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e53cfcce87e05b7f0ae2fb2b3e5820048cd0bb7b701e92bd8f75c9fbb7c9ae9", size = 364595 }, +] + +[[package]] +name = "decorator" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, +] + +[[package]] +name = "docker-pycreds" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/e6/d1f6c00b7221e2d7c4b470132c931325c8b22c51ca62417e300f5ce16009/docker-pycreds-0.4.0.tar.gz", hash = "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4", size = 8754 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/e8/f6bd1eee09314e7e6dee49cbe2c5e22314ccdb38db16c9fc72d2fa80d054/docker_pycreds-0.4.0-py2.py3-none-any.whl", hash = "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49", size = 8982 }, +] + +[[package]] +name = "ecdsa" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/d0/ec8ac1de7accdcf18cfe468653ef00afd2f609faf67c423efbd02491051b/ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8", size = 197791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/e7/ed3243b30d1bec41675b6394a1daae46349dc2b855cb83be846a5a918238/ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a", size = 149266 }, +] + +[[package]] +name = "einops" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/ca/9f5dcb8bead39959454c3912266bedc4c315839cee0e0ca9f4328f4588c1/einops-0.8.0.tar.gz", hash = "sha256:63486517fed345712a8385c100cb279108d9d47e6ae59099b07657e983deae85", size = 58861 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/5a/f0b9ad6c0a9017e62d4735daaeb11ba3b6c009d69a26141b258cd37b5588/einops-0.8.0-py3-none-any.whl", hash = "sha256:9572fb63046264a862693b0a87088af3bdc8c068fde03de63453cbbde245465f", size = 43223 }, +] + +[[package]] +name = "eth-hash" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/b6/57c89b91cf2dbb02b3019337f97bf346167d06cd23d3bde43c9fe52cae7e/eth-hash-0.7.0.tar.gz", hash = "sha256:bacdc705bfd85dadd055ecd35fd1b4f846b671add101427e089a4ca2e8db310a", size = 12463 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f0/a35e791bd73fa425838d8d0157754150ded141a94cf30d567dfeb9d57316/eth_hash-0.7.0-py3-none-any.whl", hash = "sha256:b8d5a230a2b251f4a291e3164a23a14057c4a6de4b0aa4a16fa4dc9161b57e2f", size = 8650 }, +] + +[[package]] +name = "eth-keys" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/4a/aabe0bff4e299858845fba5598c435f2bee0646366b9635750133904e2d8/eth_keys-0.6.0.tar.gz", hash = "sha256:ba33230f851d02c894e83989185b21d76152c49b37e35b61b1d8a6d9f1d20430", size = 28944 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ee/583612eed5d49f10bd1749d7dda9e93691ab02724b7af84830046e31c64c/eth_keys-0.6.0-py3-none-any.whl", hash = "sha256:b396fdfe048a5bba3ef3990739aec64901eb99901c03921caa774be668b1db6e", size = 21210 }, +] + +[[package]] +name = "eth-typing" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/32/d5a1bdf872f92a7c3361396b684aeba7abaabb341bd22a80029abcd1f68e/eth_typing-5.0.1.tar.gz", hash = "sha256:83debf88c9df286db43bb7374974681ebcc9f048fac81be2548dbc549a3203c0", size = 22716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/ef/a66ff9b83df51b83c1af468fc7b5e4a3855d9e3c01e2365ecfe1c5e84077/eth_typing-5.0.1-py3-none-any.whl", hash = "sha256:f30d1af16aac598f216748a952eeb64fbcb6e73efa691d2de31148138afe96de", size = 20085 }, +] + +[[package]] +name = "eth-utils" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cytoolz", marker = "implementation_name == 'cpython'" }, + { name = "eth-hash" }, + { name = "eth-typing" }, + { name = "toolz", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/1f/b112416a32cae14cf986f22f85abcccec54054d3d4c699ce831faaf7bf37/eth-utils-2.2.2.tar.gz", hash = "sha256:5ca6265177ce544d9d43cdf2272ae2227e5d6d9529c270bbb707d17339087101", size = 21129 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/3d/f01836312cd8b4a8768546e78b48feb52375123e2f4343119b27e78db9b9/eth_utils-2.2.2-py3-none-any.whl", hash = "sha256:2580a8065273f62ca1ec4c175228c52e626a5f1007e965d2117e5eca1a93cae8", size = 23893 }, +] + +[[package]] +name = "fastapi" +version = "0.110.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/22/7b9ee50b0a8c48f076a111d6e4071a9d4c25623dc67689c5f3aa375f779b/fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626", size = 287508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/d1/5958526c3bdbed74f88bf69b86506db5b25a600207f0f688473667690de6/fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32", size = 91834 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, + { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, + { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, + { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, + { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, + { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, + { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, + { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, + { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, + { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, + { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, + { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, + { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, + { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, + { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, + { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, + { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, + { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, + { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, + { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, + { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, + { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, + { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, + { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, + { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, + { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "fsspec" +version = "2024.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/52/f16a068ebadae42526484c31f4398e62962504e5724a8ba5dc3409483df2/fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493", size = 286853 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641 }, +] + +[[package]] +name = "fuzzywuzzy" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/4b/0a002eea91be6048a2b5d53c5f1b4dafd57ba2e36eea961d05086d7c28ce/fuzzywuzzy-0.18.0.tar.gz", hash = "sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8", size = 28888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ff/74f23998ad2f93b945c0309f825be92e04e0348e062026998b5eefef4c33/fuzzywuzzy-0.18.0-py2.py3-none-any.whl", hash = "sha256:928244b28db720d1e0ee7587acf660ea49d7e4c632569cad4f1cd7e68a5f0993", size = 18272 }, +] + +[[package]] +name = "gitdb" +version = "4.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/0d/bbb5b5ee188dec84647a4664f3e11b06ade2bde568dbd489d9d64adef8ed/gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b", size = 394469 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/5b/8f0c4a5bb9fd491c277c21eff7ccae71b47d43c4446c9d0c6cff2fe8c2c4/gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", size = 62721 }, +] + +[[package]] +name = "gitpython" +version = "3.1.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/a1/106fd9fa2dd989b6fb36e5893961f82992cf676381707253e0bf93eb1662/GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", size = 214149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/bd/cc3a402a6439c15c3d4294333e13042b915bbeab54edc457c723931fed3f/GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff", size = 207337 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "huggingface-hub" +version = "0.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/66/fa78b1cbcae512a30c2d4c702eba0e3a771ad7b304f85d5df0b339ad82f7/huggingface_hub-0.26.3.tar.gz", hash = "sha256:90e1fe62ffc26757a073aaad618422b899ccf9447c2bba8c902a90bef5b42e1d", size = 375690 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9b/3068fb3ae0b498eb66960ca5f4d92a81c91458cacd4dc17bfa6d40ce90fb/huggingface_hub-0.26.3-py3-none-any.whl", hash = "sha256:e66aa99e569c2d5419240a9e553ad07245a5b1300350bfbc5a4945cf7432991b", size = 447570 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "levenshtein" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rapidfuzz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/e6/79807d3b59a67dd78bb77072ca6a28d8db0935161fecf935e6c38c5f6825/levenshtein-0.26.1.tar.gz", hash = "sha256:0d19ba22330d50609b2349021ec3cf7d905c6fe21195a2d0d876a146e7ed2575", size = 374307 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b4/86e447173ca8d936b7ef270d21952a0053e799040e73b843a4a5ac9a15a1/levenshtein-0.26.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44c51f5d33b3cfb9db518b36f1288437a509edd82da94c4400f6a681758e0cb6", size = 177037 }, + { url = "https://files.pythonhosted.org/packages/27/b3/e15e14e5836dfc23ed014c21b307cbf77b3c6fd75e11d0675ce9a0d43b31/levenshtein-0.26.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56b93203e725f9df660e2afe3d26ba07d71871b6d6e05b8b767e688e23dfb076", size = 157478 }, + { url = "https://files.pythonhosted.org/packages/32/f1/f4d0904c5074e4e9d33dcaf304144e02eae9eec9d61b63bf17b1108ce228/levenshtein-0.26.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:270d36c5da04a0d89990660aea8542227cbd8f5bc34e9fdfadd34916ff904520", size = 153873 }, + { url = "https://files.pythonhosted.org/packages/f9/0d/cd5abe809421ce0d4a2cae60fd2fdf62cb43890068515a8a0069e2b17894/levenshtein-0.26.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:480674c05077eeb0b0f748546d4fcbb386d7c737f9fff0010400da3e8b552942", size = 186850 }, + { url = "https://files.pythonhosted.org/packages/a8/69/03f4266ad83781f2602b1976a2e5a98785c148f9bfc77c343e5aa1840f64/levenshtein-0.26.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13946e37323728695ba7a22f3345c2e907d23f4600bc700bf9b4352fb0c72a48", size = 187527 }, + { url = "https://files.pythonhosted.org/packages/36/fa/ec3be1162b1a757f80e713220470fe5b4db22e23f886f50ac59a48f0a84d/levenshtein-0.26.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceb673f572d1d0dc9b1cd75792bb8bad2ae8eb78a7c6721e23a3867d318cb6f2", size = 162673 }, + { url = "https://files.pythonhosted.org/packages/9e/d6/dc8358b6a4174f413532aa27463dc4d167ac25742826f58916bb6e6417b1/levenshtein-0.26.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42d6fa242e3b310ce6bfd5af0c83e65ef10b608b885b3bb69863c01fb2fcff98", size = 250413 }, + { url = "https://files.pythonhosted.org/packages/57/5e/a87bf39686482a1df000fdc265fdd812f0cd316d5fb0a25f52654504a82b/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8b68295808893a81e0a1dbc2274c30dd90880f14d23078e8eb4325ee615fc68", size = 1078713 }, + { url = "https://files.pythonhosted.org/packages/c5/04/30ab2f27c4ff7d6d98b3bb6bf8541521535ad2d05e50ac8fd00ab701c080/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b01061d377d1944eb67bc40bef5d4d2f762c6ab01598efd9297ce5d0047eb1b5", size = 1331174 }, + { url = "https://files.pythonhosted.org/packages/e4/68/9c7f60ccb097a86420d058dcc3f575e6b3d663b3a5cde3651443f7087e14/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9d12c8390f156745e533d01b30773b9753e41d8bbf8bf9dac4b97628cdf16314", size = 1207733 }, + { url = "https://files.pythonhosted.org/packages/64/21/222f54a1a654eca1c1cd015d32d972d70529eb218d469d516f13eac2149d/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:48825c9f967f922061329d1481b70e9fee937fc68322d6979bc623f69f75bc91", size = 1356116 }, + { url = "https://files.pythonhosted.org/packages/6f/65/681dced2fa798ea7882bff5682ab566689a4920006ed9aca4fd8d1edb2d2/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8ec137170b95736842f99c0e7a9fd8f5641d0c1b63b08ce027198545d983e2b", size = 1135459 }, + { url = "https://files.pythonhosted.org/packages/a1/e8/1ff8a634c428ed908d20482f77491cca08fa16c96738ad82d9219da138a1/levenshtein-0.26.1-cp311-cp311-win32.whl", hash = "sha256:798f2b525a2e90562f1ba9da21010dde0d73730e277acaa5c52d2a6364fd3e2a", size = 87265 }, + { url = "https://files.pythonhosted.org/packages/8f/fb/44e9747558a7381ea6736e10ac2f871414007915afb94efac423e68cf441/levenshtein-0.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:55b1024516c59df55f1cf1a8651659a568f2c5929d863d3da1ce8893753153bd", size = 98518 }, + { url = "https://files.pythonhosted.org/packages/04/90/c476a74d8ec25d680b9cbf51966d638623a82a2fd4e99b988a383f22a681/levenshtein-0.26.1-cp311-cp311-win_arm64.whl", hash = "sha256:e52575cbc6b9764ea138a6f82d73d3b1bc685fe62e207ff46a963d4c773799f6", size = 88086 }, + { url = "https://files.pythonhosted.org/packages/4c/53/3685ee7fbe9b8eb4b82d8045255e59dd6943f94e8091697ef3808e7ecf63/levenshtein-0.26.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc741ca406d3704dc331a69c04b061fc952509a069b79cab8287413f434684bd", size = 176447 }, + { url = "https://files.pythonhosted.org/packages/82/7f/7d6fe9b76bd030200f8f9b162f3de862d597804d292af292ec3ce9ae8bee/levenshtein-0.26.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:821ace3b4e1c2e02b43cf5dc61aac2ea43bdb39837ac890919c225a2c3f2fea4", size = 157589 }, + { url = "https://files.pythonhosted.org/packages/bc/d3/44539e952df93c5d88a95a0edff34af38e4f87330a76e8335bfe2c0f31bf/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92694c9396f55d4c91087efacf81297bef152893806fc54c289fc0254b45384", size = 153306 }, + { url = "https://files.pythonhosted.org/packages/ba/fe/21443c0c50824314e2d2ce7e1e9cd11d21b3643f3c14da156b15b4d399c7/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51ba374de7a1797d04a14a4f0ad3602d2d71fef4206bb20a6baaa6b6a502da58", size = 184409 }, + { url = "https://files.pythonhosted.org/packages/f0/7b/c95066c64bb18628cf7488e0dd6aec2b7cbda307d93ba9ede68a21af2a7b/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7aa5c3327dda4ef952769bacec09c09ff5bf426e07fdc94478c37955681885b", size = 193134 }, + { url = "https://files.pythonhosted.org/packages/36/22/5f9760b135bdefb8cf8d663890756136754db03214f929b73185dfa33f05/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e2517e8d3c221de2d1183f400aed64211fcfc77077b291ed9f3bb64f141cdc", size = 162266 }, + { url = "https://files.pythonhosted.org/packages/11/50/6b1a5f3600caae40db0928f6775d7efc62c13dec2407d3d540bc4afdb72c/levenshtein-0.26.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9092b622765c7649dd1d8af0f43354723dd6f4e570ac079ffd90b41033957438", size = 246339 }, + { url = "https://files.pythonhosted.org/packages/26/eb/ede282fcb495570898b39a0d2f21bbc9be5587d604c93a518ece80f3e7dc/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc16796c85d7d8b259881d59cc8b5e22e940901928c2ff6924b2c967924e8a0b", size = 1077937 }, + { url = "https://files.pythonhosted.org/packages/35/41/eebe1c4a75f592d9bdc3c2595418f083bcad747e0aec52a1a9ffaae93f5c/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4370733967f5994ceeed8dc211089bedd45832ee688cecea17bfd35a9eb22b9", size = 1330607 }, + { url = "https://files.pythonhosted.org/packages/12/8e/4d34b1857adfd69c2a72d84bca1b8538d4cfaaf6fddd8599573f4281a9d1/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3535ecfd88c9b283976b5bc61265855f59bba361881e92ed2b5367b6990c93fe", size = 1197505 }, + { url = "https://files.pythonhosted.org/packages/c0/7b/6afcda1b0a0622cedaa4f7a5b3507c2384a7358fc051ccf619e5d2453bf2/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:90236e93d98bdfd708883a6767826fafd976dac8af8fc4a0fb423d4fa08e1bf0", size = 1352832 }, + { url = "https://files.pythonhosted.org/packages/21/5e/0ed4e7b5c820b6bc40e2c391633292c3666400339042a3d306f0dc8fdcb4/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:04b7cabb82edf566b1579b3ed60aac0eec116655af75a3c551fee8754ffce2ea", size = 1135970 }, + { url = "https://files.pythonhosted.org/packages/c9/91/3ff1abacb58642749dfd130ad855370e01b9c7aeaa73801964361f6e355f/levenshtein-0.26.1-cp312-cp312-win32.whl", hash = "sha256:ae382af8c76f6d2a040c0d9ca978baf461702ceb3f79a0a3f6da8d596a484c5b", size = 87599 }, + { url = "https://files.pythonhosted.org/packages/7d/f9/727f3ba7843a3fb2a0f3db825358beea2a52bc96258874ee80cb2e5ecabb/levenshtein-0.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:fd091209798cfdce53746f5769987b4108fe941c54fb2e058c016ffc47872918", size = 98809 }, + { url = "https://files.pythonhosted.org/packages/d4/f4/f87f19222d279dbac429b9bc7ccae271d900fd9c48a581b8bc180ba6cd09/levenshtein-0.26.1-cp312-cp312-win_arm64.whl", hash = "sha256:7e82f2ea44a81ad6b30d92a110e04cd3c8c7c6034b629aca30a3067fa174ae89", size = 88227 }, + { url = "https://files.pythonhosted.org/packages/7e/d6/b4b522b94d7b387c023d22944590befc0ac6b766ac6d197afd879ddd77fc/levenshtein-0.26.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:790374a9f5d2cbdb30ee780403a62e59bef51453ac020668c1564d1e43438f0e", size = 175836 }, + { url = "https://files.pythonhosted.org/packages/25/76/06d1e26a8e6d0de68ef4a157dd57f6b342413c03550309e4aa095a453b28/levenshtein-0.26.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7b05c0415c386d00efda83d48db9db68edd02878d6dbc6df01194f12062be1bb", size = 157036 }, + { url = "https://files.pythonhosted.org/packages/7e/23/21209a9e96b878aede3bea104533866762ba621e36fc344aa080db5feb02/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3114586032361722ddededf28401ce5baf1cf617f9f49fb86b8766a45a423ff", size = 153326 }, + { url = "https://files.pythonhosted.org/packages/06/38/9fc68685fffd8863b13864552eba8f3eb6a82a4dc558bf2c6553c2347d6c/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2532f8a13b68bf09f152d906f118a88da2063da22f44c90e904b142b0a53d534", size = 183693 }, + { url = "https://files.pythonhosted.org/packages/f6/82/ccd7bdd7d431329da025e649c63b731df44f8cf31b957e269ae1c1dc9a8e/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:219c30be6aa734bf927188d1208b7d78d202a3eb017b1c5f01ab2034d2d4ccca", size = 190581 }, + { url = "https://files.pythonhosted.org/packages/6e/c5/57f90b4aea1f89f853872b27a5a5dbce37b89ffeae42c02060b3e82038b2/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397e245e77f87836308bd56305bba630010cd8298c34c4c44bd94990cdb3b7b1", size = 162446 }, + { url = "https://files.pythonhosted.org/packages/fc/da/df6acca738921f896ce2d178821be866b43a583f85e2d1de63a4f8f78080/levenshtein-0.26.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeff6ea3576f72e26901544c6c55c72a7b79b9983b6f913cba0e9edbf2f87a97", size = 247123 }, + { url = "https://files.pythonhosted.org/packages/22/fb/f44a4c0d7784ccd32e4166714fea61e50f62b232162ae16332f45cb55ab2/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a19862e3539a697df722a08793994e334cd12791e8144851e8a1dee95a17ff63", size = 1077437 }, + { url = "https://files.pythonhosted.org/packages/f0/5e/d9b9e7daa13cc7e2184a3c2422bb847f05d354ce15ba113b20d83e9ab366/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:dc3b5a64f57c3c078d58b1e447f7d68cad7ae1b23abe689215d03fc434f8f176", size = 1330362 }, + { url = "https://files.pythonhosted.org/packages/bf/67/480d85bb516798014a6849be0225b246f35df4b54499c348c9c9e311f936/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bb6c7347424a91317c5e1b68041677e4c8ed3e7823b5bbaedb95bffb3c3497ea", size = 1198721 }, + { url = "https://files.pythonhosted.org/packages/9a/7d/889ff7d86903b6545665655627113d263c88c6d596c68fb09a640ee4f0a7/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b817376de4195a207cc0e4ca37754c0e1e1078c2a2d35a6ae502afde87212f9e", size = 1351820 }, + { url = "https://files.pythonhosted.org/packages/b9/29/cd42273150f08c200ed2d1879486d73502ee35265f162a77952f101d93a0/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b50c3620ff47c9887debbb4c154aaaac3e46be7fc2e5789ee8dbe128bce6a17", size = 1135747 }, + { url = "https://files.pythonhosted.org/packages/1d/90/cbcfa3dd86023e82036662a19fec2fcb48782d3f9fa322d44dc898d95a5d/levenshtein-0.26.1-cp313-cp313-win32.whl", hash = "sha256:9fb859da90262eb474c190b3ca1e61dee83add022c676520f5c05fdd60df902a", size = 87318 }, + { url = "https://files.pythonhosted.org/packages/83/73/372edebc79fd09a8b2382cf1244d279ada5b795124f1e1c4fc73d9fbb00f/levenshtein-0.26.1-cp313-cp313-win_amd64.whl", hash = "sha256:8adcc90e3a5bfb0a463581d85e599d950fe3c2938ac6247b29388b64997f6e2d", size = 98418 }, + { url = "https://files.pythonhosted.org/packages/b2/6d/f0160ea5a7bb7a62b3b3d56e9fc5024b440cb59555a90be2347abf2e7888/levenshtein-0.26.1-cp313-cp313-win_arm64.whl", hash = "sha256:c2599407e029865dc66d210b8804c7768cbdbf60f061d993bb488d5242b0b73e", size = 87792 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, + { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, + { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, + { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, + { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, + { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, + { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, + { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, + { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, + { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, + { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, + { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, + { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, + { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, + { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, + { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, + { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, + { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, + { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, + { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, +] + +[[package]] +name = "msgpack-numpy-opentensor" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/69/2a6af13c3be6934a9ba149120a78bf63cf1455ddb1d11ec2cc5e5d6f8186/msgpack-numpy-opentensor-0.5.0.tar.gz", hash = "sha256:213232c20e2efd528ec8a9882b605e8ad87cfc35b57dfcfefe05d33aaaabe574", size = 9661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/22/590508afb85d5c27ebcb2837410413f4613eebdda6e4e02997fe08ba78e4/msgpack_numpy_opentensor-0.5.0-py2.py3-none-any.whl", hash = "sha256:8a61c597a976425a87094d8e89846aa9528eb1f037e97ff1428fe3cd61a238e7", size = 7209 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, + { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, + { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, + { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, + { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, + { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, + { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, + { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, + { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, + { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, + { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, + { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, + { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "munch" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/a1/ec48010724eedfe2add68eb7592a0d238590e14e08b95a4ffb3c7b2f0808/munch-2.5.0.tar.gz", hash = "sha256:2d735f6f24d4dba3417fa448cae40c6e896ec1fdab6cdb5e6510999758a4dbd2", size = 17015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/ab/85d8da5c9a45e072301beb37ad7f833cd344e04c817d97e0cc75681d248f/munch-2.5.0-py2.py3-none-any.whl", hash = "sha256:6f44af89a2ce4ed04ff8de41f70b226b984db10a91dcc7b9ac2efc1c77022fdd", size = 10347 }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "netaddr" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/90/188b2a69654f27b221fba92fda7217778208532c962509e959a9cee5229d/netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a", size = 2260504 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023 }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.4.5.8" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/7f/7fbae15a3982dc9595e49ce0f19332423b260045d0a6afe93cdbe2f1f624/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3", size = 363333771 }, + { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/b5/9fb3d00386d3361b03874246190dfec7b206fd74e6e287b26a8fcb359d95/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a", size = 12354556 }, + { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/aa/083b01c427e963ad0b314040565ea396f914349914c298556484f799e61b/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198", size = 24133372 }, + { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/aa/b656d755f474e2084971e9a297def515938d56b466ab39624012070cb773/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3", size = 894177 }, + { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.1.0.70" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/8a/0e728f749baca3fbeffad762738276e5df60851958be7783af121a7221e7/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399", size = 211422548 }, + { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.5.147" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/9c/a79180e4d70995fdf030c6946991d0171555c6edf95c265c6b2bf7011112/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9", size = 56314811 }, + { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.6.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/6b/a5c33cf16af09166845345275c34ad2190944bcc6026797a39f8e0a282e0/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e", size = 127634111 }, + { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.3.1.170" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/a9/c0d2f83a53d40a4a41be14cea6a0bf9e668ffcf8b004bd65633f433050c0/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3", size = 207381987 }, + { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.21.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/45/239d52c05074898a80a900f49b1615d81c07fceadd5ad6c4f86a987c0bc4/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83", size = 20552510 }, + { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/39/471f581edbb7804b39e8063d92fc8305bdc7a80ae5c07dbe6ea5c50d14a5/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3", size = 100417 }, + { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "password-strength" +version = "0.0.3.post2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/f1/6165ebcca27fca3f1d63f8c3a45805c2ed8568be4d09219a2aa45e792c14/password_strength-0.0.3.post2.tar.gz", hash = "sha256:bf4df10a58fcd3abfa182367307b4fd7b1cec518121dd83bf80c1c42ba796762", size = 12857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/d6/08fd888c980589e4e27c2a4177e972481e8881600138e63afb785fe52630/password_strength-0.0.3.post2-py2.py3-none-any.whl", hash = "sha256:6739357c2863d707b7c7f247ff7c6882a70904a18d12c9aaf98f8b95da176fb9", size = 12167 }, +] + +[[package]] +name = "pip" +version = "24.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 }, + { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 }, + { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 }, + { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 }, + { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 }, + { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 }, + { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 }, + { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 }, + { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 }, + { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 }, + { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 }, + { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 }, + { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 }, + { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 }, + { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 }, + { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 }, + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +] + +[[package]] +name = "protobuf" +version = "5.29.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/4f/1639b7b1633d8fd55f216ba01e21bf2c43384ab25ef3ddb35d85a52033e8/protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", size = 424965 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c7/28669b04691a376cf7d0617d612f126aa0fff763d57df0142f9bf474c5b8/protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", size = 422706 }, + { url = "https://files.pythonhosted.org/packages/e3/33/dc7a7712f457456b7e0b16420ab8ba1cc8686751d3f28392eb43d0029ab9/protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", size = 434505 }, + { url = "https://files.pythonhosted.org/packages/e5/39/44239fb1c6ec557e1731d996a5de89a9eb1ada7a92491fcf9c5d714052ed/protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18", size = 417822 }, + { url = "https://files.pythonhosted.org/packages/fb/4a/ec56f101d38d4bef2959a9750209809242d86cf8b897db00f2f98bfa360e/protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", size = 319572 }, + { url = "https://files.pythonhosted.org/packages/04/52/c97c58a33b3d6c89a8138788576d372a90a6556f354799971c6b4d16d871/protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", size = 319671 }, + { url = "https://files.pythonhosted.org/packages/3b/24/c8c49df8f6587719e1d400109b16c10c6902d0c9adddc8fff82840146f99/protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", size = 172547 }, +] + +[[package]] +name = "psutil" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/2b/f4dea5d993d9cd22ad958eea828a41d5d225556123d372f02547c29c4f97/psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e", size = 246648 }, + { url = "https://files.pythonhosted.org/packages/9f/14/4aa97a7f2e0ac33a050d990ab31686d651ae4ef8c86661fef067f00437b9/psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85", size = 249905 }, + { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, + { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, + { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, + { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, + { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, + { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, + { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, +] + +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, +] + +[[package]] +name = "py-bip39-bindings" +version = "0.1.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/8a/5e22cbd00b799b33ce0a45ae3715c9ea3fcd263f877544819e7d03753c49/py_bip39_bindings-0.1.11.tar.gz", hash = "sha256:ebc128ccf3a0750d758557e094802f0975c3760a939f8a8b76392d7dbe6b52a1", size = 18103 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b6/0bd5bf1c4cb00e000a9c909280aa7f8654208ee136e2cd1f3650a8de59ed/py_bip39_bindings-0.1.11-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:27cce22727e28705a660464689ade6d2cdad4e622bead5bde2ffa53c4f605ee5", size = 399429 }, + { url = "https://files.pythonhosted.org/packages/22/44/b6ffdc17cc499b72821a1d777fb465af842d5b7373f1825a45dce551fedc/py_bip39_bindings-0.1.11-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:cdf35d031587296dcbdb22dbc67f2eaf5b5df9d5036b77fbeb93affbb9eec8d3", size = 792364 }, + { url = "https://files.pythonhosted.org/packages/15/8d/0883d814a26f922b331218c777ecaec61919aebf9c54d4991f919b21ab8a/py_bip39_bindings-0.1.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2fd5b926686207752d5f2e2ff164a9489b3613239d0967362f10c2fbd64eb018", size = 413967 }, + { url = "https://files.pythonhosted.org/packages/72/30/e3c76035b83c9552bbeee90645411a3d52983067badbd8a5854a823701f9/py_bip39_bindings-0.1.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba84c38962bffdaea0e499245731d669cc21d1280f81ace8ff60ed3550024570", size = 1225281 }, + { url = "https://files.pythonhosted.org/packages/38/c9/3b73fe8ffd285387c4fe7b60ccd0072ee16d5153409619c472852ec88acc/py_bip39_bindings-0.1.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9024ec3c4a3db005b355f9a00602cede290dec5e9c7cf7dd06a26f620b0cf99", size = 1227054 }, + { url = "https://files.pythonhosted.org/packages/7e/2f/d096e6e08439e5b3c1f41e95c5828700012c130611a64fe9e82a43b0ca45/py_bip39_bindings-0.1.11-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:ce028c8aef51dec2a85f298461b2988cca28740bf3cc23472c3469d3f853714e", size = 1180933 }, + { url = "https://files.pythonhosted.org/packages/0b/8f/00c2239452f26e180229d74acd63ac0c027f8eb9a5fb90b879c6c1192102/py_bip39_bindings-0.1.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:51882cd0fa7529173b3543c089c24c775f1876ddf48f10e60f2ed07ad2af5cae", size = 1264074 }, + { url = "https://files.pythonhosted.org/packages/1a/7a/524e38494a0ffb7ca211225acde0324cf216f081dffae7fd55446b009889/py_bip39_bindings-0.1.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4ee776f3b33b2d71fee48679951f117e3d1f052449ec2fcb184f3c64a4c77e4f", size = 1252039 }, + { url = "https://files.pythonhosted.org/packages/e9/a0/68bbb9e9326266a9acca2558d6556e22df31bcf4d2235ee1cdaf362add82/py_bip39_bindings-0.1.11-cp311-none-win32.whl", hash = "sha256:d8b722e49562810f94eb61c9efa172f327537c74c37da3e86b161f7f444c51bf", size = 296767 }, + { url = "https://files.pythonhosted.org/packages/0a/47/4c5d0ff9949b725696b1b10b5b87f6c6d3c333d8458b354b7c8536272eef/py_bip39_bindings-0.1.11-cp311-none-win_amd64.whl", hash = "sha256:be934052497f07605768e2c7184e4f4269b3e2e77930131dfc9bdbb791e6fdf4", size = 283550 }, + { url = "https://files.pythonhosted.org/packages/cb/a5/0d29c79ee79475ceca80ca19b5975917827af6ce4dd2711ed197822a12ea/py_bip39_bindings-0.1.11-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:afa9c5762cfaec01141f478a9c3132de01ec3890ff2e5a4013c79d3ba3aff8bb", size = 798236 }, + { url = "https://files.pythonhosted.org/packages/47/fd/a4baff5368ef8be569064e5aef1319c4e75b24a80c70c0f3a871727c6a38/py_bip39_bindings-0.1.11-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a3af7c1f955c6bbd613c6b38d022f7c73896acaf0ecc972ac0dee4b952e14568", size = 406227 }, + { url = "https://files.pythonhosted.org/packages/78/44/fe4a107204690d18691a2db7cacfd6043331f6982dc59962d9e220d46711/py_bip39_bindings-0.1.11-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6aed3e86f105a36676e8dd0c8bc3f611a81b7ba4309b22a77fdc0f63b260e094", size = 1215916 }, + { url = "https://files.pythonhosted.org/packages/0d/53/0cbfe92fde6925244280eaed3ede0f16cb498c8764023acc155225d5f9e4/py_bip39_bindings-0.1.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d202f051cf063abae3acd0b74454d9d7b1dbeaf466ef7cb47a34ccedac845b62", size = 451663 }, + { url = "https://files.pythonhosted.org/packages/44/9b/4c3c8c6decdc7472323a66e98e1d37c43dcbf798c944791eafeb63ff8411/py_bip39_bindings-0.1.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae120b5542fecf97aa3fdb6a526bac1004cb641bc9cc0d0030c6735dc2156072", size = 1206493 }, + { url = "https://files.pythonhosted.org/packages/94/47/71ed526077a4e58ac4ec5dbb43637faa33cc02a0ada912a3fd8f20c193b9/py_bip39_bindings-0.1.11-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:baf896aabb3bec42803015e010c121c8a3210b20184f37aaa6e400ae8e877e60", size = 483935 }, + { url = "https://files.pythonhosted.org/packages/be/e3/7da98b60d113334e2eb95028289410f8a1771e755fa7ad3de1ae2fa9d951/py_bip39_bindings-0.1.11-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e4d45324c598197dbddac10a0298197ca2587fa7b09d1450697517988a29d515", size = 481093 }, + { url = "https://files.pythonhosted.org/packages/c1/38/d54060bda276a062e2327e169b6660b27beb4f75ab7a9e216dd11b9ae703/py_bip39_bindings-0.1.11-cp312-none-win32.whl", hash = "sha256:92abce265b0f2d8c5830441aff06b7b4f9426088a3de39624b12f3f9ff9fc2eb", size = 296429 }, + { url = "https://files.pythonhosted.org/packages/86/12/256aa92f70a8bdf2a00dc84f6c75c86abadeca1c990e02c8345933889952/py_bip39_bindings-0.1.11-cp312-none-win_amd64.whl", hash = "sha256:6794187229eb0b04d0770f0fba936f0c5c598f552848a398ed5af9a61638cacb", size = 284888 }, +] + +[[package]] +name = "py-ed25519-zebra-bindings" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/f5/833c284f7d76e13d9520215b5731be2983f8f04cf3405f087267de89af37/py_ed25519_zebra_bindings-1.1.0.tar.gz", hash = "sha256:2977603b59cfc593fb01284465fe41062d6929b0d09edf0e1ade40709977014f", size = 12176 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/95/ae7129025ffc3954994b0bd72c83a091ec1a96a508da2b5a8f3e9e54ef93/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:048e84121007b6ced32b70086e9bd710a825920f0715d73be4760c45f61847be", size = 287381 }, + { url = "https://files.pythonhosted.org/packages/46/9d/f41a6b8103697eca40c0bb9b22c8bd9f9593ed1941da340ce27b419c2d6a/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8200479a222da9bab0abbe35d9a60e4f658a4039054c3b9f2e58a102a393a658", size = 263398 }, + { url = "https://files.pythonhosted.org/packages/fa/38/5edf90bea230aa9528f581b5540f994bba6bd517ffc1a902d44fdc46fcc5/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ef039b7a7e4f1653e7f0c467d136a7e135061e53fdc74934d36489a8859f9e4", size = 295474 }, + { url = "https://files.pythonhosted.org/packages/ab/76/79e2a9e3873b7f99b3071a3f2473d7355d47adfa576843fa084bfd1e66e5/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6d035b7bd3dd998ef6030666e69cde95c34225187f53ebb9c2fa7298a65ffde", size = 321097 }, + { url = "https://files.pythonhosted.org/packages/12/de/5aa80345871578a23d548f03597cab77ec7269434136a0e1f716f7222355/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f4a7bb72294f7f1f560a464832c0fc32bc0a20cf4d3b638f2428bf3dde6ebda", size = 334803 }, + { url = "https://files.pythonhosted.org/packages/a1/c9/a6819824ed0bea5139d3b85eb51343f2f00dcfc93cdf3829deeac8b63fc9/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89735c2623bcf02177004eaa895250a3115214cd51df2ab561863f565aa06b1b", size = 316386 }, + { url = "https://files.pythonhosted.org/packages/49/c3/37d32d12e36226f6ffb3d5f68e2191e56b708bfc39662f67e1ff7c5244ee/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a7cccd8ab3156d1f397c6476583e78427e93cd01fa82173df78b96e15eb9f4d", size = 334029 }, + { url = "https://files.pythonhosted.org/packages/28/82/82e787a9899a02154b7db6f73c3fe9d5f414baa45e3f4dff00362750473f/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27483aa9262d0952e886d01ec174056503238064ce1f08a3fb17752db18071dd", size = 481082 }, + { url = "https://files.pythonhosted.org/packages/d8/a7/e37725ae7029482742ce77887636a1051c8ba4667c84dca53c78e631d2e2/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b461baeb4adb5c5d916f8cf31651142744f29b90f010a71bb22beafe0d803f40", size = 583309 }, + { url = "https://files.pythonhosted.org/packages/89/e4/568c2134ebe3b95ef10c0d00da28a8a667aaa2cf913f2b8436a7080f8036/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b79af368be80b5cd32b2a678c775f113c1d76c6f0e1ea5e66586c81c9e0ab5b", size = 506769 }, + { url = "https://files.pythonhosted.org/packages/08/06/1afb277b1021cc0669b56fe7cad81b5f26f6cded280f1b2f881ac6dd5f54/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9555ccf645374e5282103416fe5cba60937d7bf12af676980bd4e18cfa2fab48", size = 485460 }, + { url = "https://files.pythonhosted.org/packages/21/82/576d2c725d6fb66090424ad30eae9b1a503cd07973b6bc10c1f28a508686/py_ed25519_zebra_bindings-1.1.0-cp311-none-win32.whl", hash = "sha256:1c55a32c2070aa68e0ed5a2930ba547fbf47617fd279462171d5c0f87b00df6d", size = 183764 }, + { url = "https://files.pythonhosted.org/packages/2f/ed/1da5f72b31eda77d6333756c6f4c542962ab6071a192e3bf4db0ca1e4ccb/py_ed25519_zebra_bindings-1.1.0-cp311-none-win_amd64.whl", hash = "sha256:c4a4dedb1b8edf7f68dd8015f9d8a20f2f0ecca90fac4432e5cbabfcc16ab13d", size = 186734 }, + { url = "https://files.pythonhosted.org/packages/01/e7/41d2c8c43d173e3f5421dbcff89ddec60f3b0df2111146a3469be8f0b19f/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3ee9a0b7eb896547e539a27c4286461d58c6a99952ea27fa1b5f5e39e63019dc", size = 287118 }, + { url = "https://files.pythonhosted.org/packages/81/99/4dd0de0907eb44e3bb4240dd5daa002ab52c92a44f288cb8485e1cd42534/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93e2a12d0bbf58f4d1e5ae2a1c352e43302cadd747a1a5e88fea03ce7a78a562", size = 263119 }, + { url = "https://files.pythonhosted.org/packages/99/ea/46d2ffbe8205c02228106ff757bd1afebfe1258f8ded83303ed20c689499/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33673e6047eba0a0a28e818fa0b36b703986347fc98e6f0f96e36af68756787", size = 294870 }, + { url = "https://files.pythonhosted.org/packages/ad/6c/767881f4917b7626a1a1e5ad80b031a692883e93076c361254f20bcf3964/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14399e6e8c5b9c193256a1b9aec16b9de719691de84ab23a690056cfe011d13b", size = 320685 }, + { url = "https://files.pythonhosted.org/packages/1a/4c/4e730ff1c965bacd6a6a065cd4e462d37ed938d172939609a5da01bc03f5/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85ea7a5632a1bf6713518bb56d4c8abe5128aee173a3c498b3a564cfb346ca72", size = 333754 }, + { url = "https://files.pythonhosted.org/packages/bd/5b/b889db7a23724ac1c559c7079c7a941e822849695109aac1d065251039ff/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a01f58a1c980df68c8efd43892562b3224507bab83d880910fbb4a3c84fc965", size = 316045 }, + { url = "https://files.pythonhosted.org/packages/7f/3d/ba457c149c108d594adea964573f2b4809fd3b7ed887a7d38aa77ed7d11b/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:655360cd74718d4efb8fbaf7afb2e4ce459af5f1d399479f577a63bd9177aa3b", size = 333567 }, + { url = "https://files.pythonhosted.org/packages/88/00/f4e05b36cc04212c4d00c6fcbfbf17a2b15f6d08f00f12c1ce499061b666/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:650081644c6613fdf658456ed4b2a6580ec1b54084f318a31a924ce5cf536bb9", size = 481375 }, + { url = "https://files.pythonhosted.org/packages/be/7b/e96f39dfa9fc11e477256b3dec80b7afde51004ba97f87ac2eb3e68e44cf/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d56b81186fc75cbcf80d0549f83e98c62c4359460e512f9fb8d6c7be2a158dd", size = 583169 }, + { url = "https://files.pythonhosted.org/packages/ca/8f/9dbf2a43efdfd2b2452eb497b3a72bbac40c2fa57afc6262574cd05ed7ae/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:072bf62421ad890c1849aaa19c7b6e6a890d337f0622e9bd09161b180a10496c", size = 506486 }, + { url = "https://files.pythonhosted.org/packages/28/ca/2bad513face5cce85773d9ecf92dfe8ddbeb8ef33d64b6c2a14565cc99b3/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e09830c3672699f5f1f164fe92b102254ef68300ceaddc847d9a35bf4a2ec270", size = 485075 }, + { url = "https://files.pythonhosted.org/packages/f9/18/ebc4dcde6da7f6a5040bf4c53fd53a5942500e40da8ff3179b8e8462f62c/py_ed25519_zebra_bindings-1.1.0-cp312-none-win32.whl", hash = "sha256:33ca2a7ad10846c281a73450316b390c7343e62e40516389fc1b580241f3907f", size = 183761 }, + { url = "https://files.pythonhosted.org/packages/4f/17/e334d38d2ff14fab0722e03959472b6e24740376ae92bc30c9c076af2be8/py_ed25519_zebra_bindings-1.1.0-cp312-none-win_amd64.whl", hash = "sha256:4ba042528ddb81f8f025b1987bf8f19547f188efb7aa4c95d1a4e3e7f968e991", size = 186542 }, +] + +[[package]] +name = "py-sr25519-bindings" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/55/e5c27d1387f6cb3a6bf7714e1e0c4a62edc3b006710e2d081e8bdfa4123f/py_sr25519_bindings-0.2.1.tar.gz", hash = "sha256:1b96d3dde43adcf86ab427a9fd72b2c6291dca36eb40747df631588c16f01c1a", size = 18439 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/e5/62067ff055a940bcbb02467f7fb63fd85a89cc12153f8c78199ce5c71fb9/py_sr25519_bindings-0.2.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4941e6e0e180f7e72565043ed3ba7190455c9feaa2ab9ee6038904f2b4bb6c5b", size = 331203 }, + { url = "https://files.pythonhosted.org/packages/0a/6c/48a6e1289012b4ab704ccec5315a7c1f1694909b5cc332a36ec87ab03608/py_sr25519_bindings-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b63d7cf5bb4d9b986d7f7012c80b92be70311dc9b75862f7880e03b71a29543d", size = 306083 }, + { url = "https://files.pythonhosted.org/packages/e6/da/b7ab72a15e950779edf376b344b6de43aacc7250e319ff23996ef96cda5b/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6752bf3b109446d99f3a368e3ba805812fc5bc09e52ef1c82f5a47e43b19973", size = 340172 }, + { url = "https://files.pythonhosted.org/packages/15/7f/4defee54893a3947936f3b5b8b1fe8cb10bb6d01cf87240345f511636e8d/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0368dcdf5ec8d2bb9c13273c78c3c5b033211d37a70a2f1d2080f29a7d118340", size = 368044 }, + { url = "https://files.pythonhosted.org/packages/44/a9/b6ddb161bb28f7da1b261d8e6d59d9669a15bdbfe8bfff0ff15f9a28f0a6/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2618b02b4a3babac07b8bb61fe9550f911f038bb079665682ca76b2e664e5258", size = 384053 }, + { url = "https://files.pythonhosted.org/packages/7a/66/5d4c78ad9766cd46e5439e9fb84cb10bc47b9c4929c8ea99ee880f405f50/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab1bc4dc524efefaecf3a85f4a0ff05c1ca9509d4d64056199984550f3c98b3", size = 365700 }, + { url = "https://files.pythonhosted.org/packages/07/ef/f96d4e2472af62768ffd81df2170f643de87b0ab831e405a4572b9959379/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ccdc89d5e3ae0dd163c8150ec76b6bb3291c1cec9746eb79e9544b3423f35f9", size = 385360 }, + { url = "https://files.pythonhosted.org/packages/9e/91/ea5e750e5f2896412fcbbe32da3be8ffab50f4221df7fe3ab367c51a99ac/py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ae6545c414cfa5d7207c9c77aaa576bb374982fb2105a7a9c2764afa5621f6d4", size = 523867 }, + { url = "https://files.pythonhosted.org/packages/7c/d0/e56f6753b264dd4c3f40364879429af7127c8b235c7a2f6d5fbb69137004/py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7046774e39e0166d3c12632969c9d1713e6ad9ca8206bbe82923ba6935b0a01f", size = 627828 }, + { url = "https://files.pythonhosted.org/packages/63/19/7a8d5cca0a498da55b0457be98f03e428e4981b563e5d1c8c92dfc7d136e/py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cba9a8821176895b080ea761e5ab9cd8727660bf401478a6532a30ae3429573d", size = 551658 }, + { url = "https://files.pythonhosted.org/packages/58/4e/083694bded9ce2d8d598f086aa4ca67f2b9c5d9bfd79ca46f04c95e9322b/py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c31aba05819e5b6b26746dc1b078cf680bd471f135c55e376e95c7774e22e936", size = 529627 }, + { url = "https://files.pythonhosted.org/packages/3d/cc/837b57c938d2b1d0e6f296dc09a3e65b0d762b2387301f8a51452d679391/py_sr25519_bindings-0.2.1-cp311-none-win32.whl", hash = "sha256:d4bfb9c9a5c46563ccf12e74862ee95d2961556ba7aca62c9e4d6e4f7c37b4e0", size = 217894 }, + { url = "https://files.pythonhosted.org/packages/5e/43/3f91ccad4b8d96ddf9a26b00be11de6ad0d260ab26e17ad8f98088512c3a/py_sr25519_bindings-0.2.1-cp311-none-win_amd64.whl", hash = "sha256:4f0d5c065d5e6122e53e771035aa335534363b451358b408d211df1c46773617", size = 224191 }, + { url = "https://files.pythonhosted.org/packages/fa/6f/5dca831fe2617075237d49868d1bd4f025d0dbd23676d7dec3aaf39642cd/py_sr25519_bindings-0.2.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:01ef73c0b3d3f703b54ee69c0f5ff4aa54b4233212c466fd497c7a84d170963a", size = 330633 }, + { url = "https://files.pythonhosted.org/packages/3e/86/569b69e01a962e0c3cd63465e5faad589e54f0c27bfaed5436fef283d56c/py_sr25519_bindings-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7ce8ac85e5ea82825a863f3f6f071e5ead610d7675820eb8ffe772267445ec0b", size = 306030 }, + { url = "https://files.pythonhosted.org/packages/a1/ae/ad0d1fff92966b4ca020abc3ea12e3e1f34c3a937bab28fa0e6bf893d587/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f59ac8c03c8ef819db063627f4a8247aab0db11d88b21562abbe371612cf66ab", size = 340266 }, + { url = "https://files.pythonhosted.org/packages/b0/7e/93903b1a0789fe1e7f2bb17f4992b55549dfbc8dd8dc3fa4d57c08b72250/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2c11fc77b57308e3ada9a40e7c343027129b582d3091ebd992c99b1832ac8c1", size = 367790 }, + { url = "https://files.pythonhosted.org/packages/f4/79/842a46cc48c33ff0d08f95db6b327fdd5972fd68d733634322762dd74702/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92af2831d6896f0b3fef792d1f2da780fabf6c78dac12535b394cbdb51c0d257", size = 383790 }, + { url = "https://files.pythonhosted.org/packages/0d/33/aeeacf174483ae6163bfb8993c0dabdb15875272e59658123d2dcf55f39a/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc99f7f310b7641e510810c1d6a6b51792ab2ccefac3ab288445a9fcbc9a8265", size = 365962 }, + { url = "https://files.pythonhosted.org/packages/85/bb/c41e0115115336acad5b05d577bf463fa69975ed84dcf50011ac4e07eb89/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1dc4995a352a6e5851a41cb0ea37d8c9083d173515b7fd2f381b014f57dc1cda", size = 386028 }, + { url = "https://files.pythonhosted.org/packages/cd/d0/48744d7ec55853dc7ec6889f7b85b4f9d21349f09a9ccc8fd988a67f0a46/py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f103dc5c420057c4447bd6ebf28b2b68ff3ab8da85a5f7ff39c405293de80c78", size = 524320 }, + { url = "https://files.pythonhosted.org/packages/50/4f/9462c0525bd64417c56e788b9543a34c08583bf7eabf81797bf5545b924d/py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:902ee675497b8d356a2abe2abc4278cd76c503f76d06ef2bcd797c1df59e84b7", size = 628052 }, + { url = "https://files.pythonhosted.org/packages/a7/2a/873f8e7425fd424f9d4aa6eddbbe767889d2aee639372fd9516d6b352c93/py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5dd9748f4bd9a3bc4d5c1245f6edcc723075b1470b4c36add4474df4c53604e8", size = 552273 }, + { url = "https://files.pythonhosted.org/packages/0e/e2/bb29457851816c1637bdd7176ac419073faeecf452dcfae54b50ddb81bc1/py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c24bc55699d12948571969c26e65138a942bdaca062171288c40c44b9a4f266", size = 530013 }, + { url = "https://files.pythonhosted.org/packages/4b/70/21d32090ca207738a3979620865e2a48ccbed64871cffafb24c6febe234d/py_sr25519_bindings-0.2.1-cp312-none-win32.whl", hash = "sha256:d4799c9a8f280abdfe564d397bad45da380275c8d22604e059bd7b3d5af404b5", size = 218181 }, + { url = "https://files.pythonhosted.org/packages/bb/df/06a61ef52a6889d6879bfa8a5877688f62854c8eab491ad7af60e797a3ef/py_sr25519_bindings-0.2.1-cp312-none-win_amd64.whl", hash = "sha256:0746befd71d1766d8747910cfeb2cec2be2c859c3b3618eda1dc3cb4a1b85175", size = 224095 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pycryptodome" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, + { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, + { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, + { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, + { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, + { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, + { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, + { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, + { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, + { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 }, + { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 }, +] + +[[package]] +name = "pydantic" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 }, + { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 }, + { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 }, + { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 }, + { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 }, + { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 }, + { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 }, + { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 }, + { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 }, + { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 }, + { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 }, + { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, + { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, + { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, + { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, + { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, + { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, + { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, + { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, + { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, + { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, + { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, + { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, + { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, + { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, + { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, + { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, + { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, + { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, + { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, + { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, + { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, + { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, + { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, + { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, + { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, + { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, + { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[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 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624 }, + { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[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 } +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 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "python-levenshtein" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "levenshtein" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/72/58d77cb80b3c130d94f53a8204ffad9acfddb925b2fb5818ff9af0b3c832/python_levenshtein-0.26.1.tar.gz", hash = "sha256:24ba578e28058ebb4afa2700057e1678d7adf27e43cd1f17700c09a9009d5d3a", size = 12276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/d7/03e0453719ed89724664f781f0255949408118093dbf77a2aa2a1198b38e/python_Levenshtein-0.26.1-py3-none-any.whl", hash = "sha256:8ef5e529dd640fb00f05ee62d998d2ee862f19566b641ace775d5ae16167b2ef", size = 9426 }, +] + +[[package]] +name = "python-statemachine" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/91/4f05f3931d1e9b1df71b17dc08c43feddf2bed7dbf13f95323df2cc8e340/python_statemachine-2.5.0.tar.gz", hash = "sha256:ae88cd22e47930b92b983a2176e61d811e571b69897be2568ec812c2885fb93a", size = 403718 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2d/1c95ebe84df60d630f8e855d1df2c66368805444ac167e9b50f29eabe917/python_statemachine-2.5.0-py3-none-any.whl", hash = "sha256:0ed53846802c17037fcb2a92323f4bc0c833290fa9d17a3587c50886c1541e62", size = 50415 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "rapidfuzz" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/39/e3bcb901c2746734cd70151253bf9e61c688d3c415227b08e6fbf7eb5d7f/rapidfuzz-3.10.1.tar.gz", hash = "sha256:5a15546d847a915b3f42dc79ef9b0c78b998b4e2c53b252e7166284066585979", size = 57982250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/2c/62efddd64bcaf39c03b928784777bb614028c5975ec7465d34eded34a7f7/rapidfuzz-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:92958ae075c87fef393f835ed02d4fe8d5ee2059a0934c6c447ea3417dfbf0e8", size = 1954920 }, + { url = "https://files.pythonhosted.org/packages/41/a7/f8411b9b4037d1ea6707dee975e4ed6b5358192f5ba7aa544610df5c7522/rapidfuzz-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba7521e072c53e33c384e78615d0718e645cab3c366ecd3cc8cb732befd94967", size = 1427745 }, + { url = "https://files.pythonhosted.org/packages/0d/69/ddd0192b64cb55bca40ebcae48480fab0148334b9995eb9d5bd78b7333f6/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d02cbd75d283c287471b5b3738b3e05c9096150f93f2d2dfa10b3d700f2db9", size = 1409233 }, + { url = "https://files.pythonhosted.org/packages/18/7d/0655a52c31227bf2880f28d3c01cc4f20b584210f849a1953e4c734599e5/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa1582a397da038e2f2576c9cd49b842f56fde37d84a6b0200ffebc08d82350", size = 5609458 }, + { url = "https://files.pythonhosted.org/packages/0b/c5/5f18cd956fcf95cbdee054cd4f7b7042eacc1430f6682fae0859deb9694b/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12912acee1f506f974f58de9fdc2e62eea5667377a7e9156de53241c05fdba8", size = 1675729 }, + { url = "https://files.pythonhosted.org/packages/82/67/cf9f25a2dc02f8170c1c0b7f6d41aa39b0f28c3cd54140ec3cab315cbdf0/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666d5d8b17becc3f53447bcb2b6b33ce6c2df78792495d1fa82b2924cd48701a", size = 1678147 }, + { url = "https://files.pythonhosted.org/packages/ac/3d/fa8444d7144129b1c67a2ba0660b44af03285fd641516ee294593d2acb91/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26f71582c0d62445067ee338ddad99b655a8f4e4ed517a90dcbfbb7d19310474", size = 3129309 }, + { url = "https://files.pythonhosted.org/packages/81/f6/a9fc68b776273282d6aeaadc6330740328bac29f8746fe8cceb9155e904a/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8a2ef08b27167bcff230ffbfeedd4c4fa6353563d6aaa015d725dd3632fc3de7", size = 2339967 }, + { url = "https://files.pythonhosted.org/packages/17/e5/f6c99fefbacef3676394b09ee66782d72710911c971c8730ef602e21fbd1/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:365e4fc1a2b95082c890f5e98489b894e6bf8c338c6ac89bb6523c2ca6e9f086", size = 6943002 }, + { url = "https://files.pythonhosted.org/packages/ee/ab/92c97b37ee24f68e2f904d8ef658bcfa47e3caf4d8491aa8bc5314704fc4/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1996feb7a61609fa842e6b5e0c549983222ffdedaf29644cc67e479902846dfe", size = 2717032 }, + { url = "https://files.pythonhosted.org/packages/20/f9/894a20e7856c9b29fd746ffca8f8360df8e4027b503ac5475439c043137f/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:cf654702f144beaa093103841a2ea6910d617d0bb3fccb1d1fd63c54dde2cd49", size = 3263149 }, + { url = "https://files.pythonhosted.org/packages/db/69/2a00d3c7d29d084311b1ab0fc83ba228ce81f78e9a60f901d64c74c0f31e/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec108bf25de674781d0a9a935030ba090c78d49def3d60f8724f3fc1e8e75024", size = 4176326 }, + { url = "https://files.pythonhosted.org/packages/bd/27/0cef6ddfd7b163b99b40a7fb1b1c15e0c9d25ec8f528b9f0af9dc2b980a2/rapidfuzz-3.10.1-cp311-cp311-win32.whl", hash = "sha256:031f8b367e5d92f7a1e27f7322012f3c321c3110137b43cc3bf678505583ef48", size = 1835384 }, + { url = "https://files.pythonhosted.org/packages/fc/0b/b15a8853672e6fca00d83b3a6c037c07ff16a73932a55e69488c46e6b9d7/rapidfuzz-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:f98f36c6a1bb9a6c8bbec99ad87c8c0e364f34761739b5ea9adf7b48129ae8cf", size = 1614933 }, + { url = "https://files.pythonhosted.org/packages/95/8a/6057b41a8a6a2245a699c1beff62baa1021543e953e05dbdb355b953f886/rapidfuzz-3.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:f1da2028cb4e41be55ee797a82d6c1cf589442504244249dfeb32efc608edee7", size = 845810 }, + { url = "https://files.pythonhosted.org/packages/77/e9/a7981ad1a7fbe4d76aa4fbbc8f2d6aac289ab62e60173f92fd3e05619f25/rapidfuzz-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1340b56340896bede246f612b6ecf685f661a56aabef3d2512481bfe23ac5835", size = 1938706 }, + { url = "https://files.pythonhosted.org/packages/bd/2b/f343df6ae726d01aa31c5ff63f2a5807dfeffa671ebf2fb9be8f8b9b2026/rapidfuzz-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2316515169b7b5a453f0ce3adbc46c42aa332cae9f2edb668e24d1fc92b2f2bb", size = 1423814 }, + { url = "https://files.pythonhosted.org/packages/13/07/6accf77b78772de2a5590ef7965d3b7c9997c5a92e913e525765586aa261/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e06fe6a12241ec1b72c0566c6b28cda714d61965d86569595ad24793d1ab259", size = 1393680 }, + { url = "https://files.pythonhosted.org/packages/46/16/2a016051489f870d15f7cdcccf823ea5f474453dda5c20cf0044ed3122d5/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99c1cd9443b19164ec185a7d752f4b4db19c066c136f028991a480720472e23", size = 5545438 }, + { url = "https://files.pythonhosted.org/packages/97/0b/2cdafff5dcb06ed6ede6f81a2f677c1f4cc08a47a6cf16862eb62903a84b/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1d9aa156ed52d3446388ba4c2f335e312191d1ca9d1f5762ee983cf23e4ecf6", size = 1646447 }, + { url = "https://files.pythonhosted.org/packages/97/65/20a859278192ca036ead255dda49f4eac63dd8d666b3a902d7be3afd38ac/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54bcf4efaaee8e015822be0c2c28214815f4f6b4f70d8362cfecbd58a71188ac", size = 1672282 }, + { url = "https://files.pythonhosted.org/packages/3c/05/b8dcfbdc8f4e3e84abf86ea13ec9595ebaf7e5375011e5d49642705704ad/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0c955e32afdbfdf6e9ee663d24afb25210152d98c26d22d399712d29a9b976b", size = 3126089 }, + { url = "https://files.pythonhosted.org/packages/3f/eb/e2f5b1643cf463b1b23c36875e711cae0091f6aaa1538c025a12cba32634/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:191633722203f5b7717efcb73a14f76f3b124877d0608c070b827c5226d0b972", size = 2300501 }, + { url = "https://files.pythonhosted.org/packages/7c/28/f3aa5d3a56cc978e73baff951549425d1a722ec3b58cacbc74c4faad2127/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:195baad28057ec9609e40385991004e470af9ef87401e24ebe72c064431524ab", size = 6903454 }, + { url = "https://files.pythonhosted.org/packages/b8/66/ba6de8c1fe5c50230d4e0adb87dde39b143ac2a4ce959a3f8076266b1767/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0fff4a6b87c07366662b62ae994ffbeadc472e72f725923f94b72a3db49f4671", size = 2681137 }, + { url = "https://files.pythonhosted.org/packages/e8/ca/4e9dbc9bca8cd1b933cfc6f961179f883cead90689619ec0aa1a5f075b0e/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4ffed25f9fdc0b287f30a98467493d1e1ce5b583f6317f70ec0263b3c97dbba6", size = 3230482 }, + { url = "https://files.pythonhosted.org/packages/14/50/6484ce7091b815757d6f0c434b78b568d3e7a80b6145a2d9aadc65b16132/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d02cf8e5af89a9ac8f53c438ddff6d773f62c25c6619b29db96f4aae248177c0", size = 4147386 }, + { url = "https://files.pythonhosted.org/packages/0b/27/9f7a0dbdd5b478790c68297b0678bc0088b9068e667878e5d1359c3ffba0/rapidfuzz-3.10.1-cp312-cp312-win32.whl", hash = "sha256:f3bb81d4fe6a5d20650f8c0afcc8f6e1941f6fecdb434f11b874c42467baded0", size = 1818115 }, + { url = "https://files.pythonhosted.org/packages/58/b6/c5f5e8043052fdbd4aa4f41d93b0e72d089172237ed5ec42118683a9833a/rapidfuzz-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:aaf83e9170cb1338922ae42d320699dccbbdca8ffed07faeb0b9257822c26e24", size = 1600653 }, + { url = "https://files.pythonhosted.org/packages/56/d3/dd84c7ed88cd4391e78b3579ecf7141ebf8b900097da2d6911db148d4bb6/rapidfuzz-3.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:c5da802a0d085ad81b0f62828fb55557996c497b2d0b551bbdfeafd6d447892f", size = 840363 }, + { url = "https://files.pythonhosted.org/packages/2f/7a/18aa6a51345e46886784e90a2c5bba62e45ee3adc554c12c3b97c297c4c3/rapidfuzz-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc22d69a1c9cccd560a5c434c0371b2df0f47c309c635a01a913e03bbf183710", size = 1931333 }, + { url = "https://files.pythonhosted.org/packages/6f/6a/7e34ddc3d6d751c4dba0d58b681c99f161225730e9a2fa71969d2fa1d281/rapidfuzz-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38b0dac2c8e057562b8f0d8ae5b663d2d6a28c5ab624de5b73cef9abb6129a24", size = 1417685 }, + { url = "https://files.pythonhosted.org/packages/ca/15/93a2eafbb4cc563d72112e4717b8c6f09e9de15f5a7709b832b8c9b81196/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fde3bbb14e92ce8fcb5c2edfff72e474d0080cadda1c97785bf4822f037a309", size = 1388805 }, + { url = "https://files.pythonhosted.org/packages/82/23/541da9279b21fc380e89e49e5acd863ba2e2b5d483eb5b6e0cfc427e4540/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9141fb0592e55f98fe9ac0f3ce883199b9c13e262e0bf40c5b18cdf926109d16", size = 5515246 }, + { url = "https://files.pythonhosted.org/packages/f3/a0/f0e43fdaf3c3c1907aa377d3610c70b31830e4d6915b8a61b51b064fcbce/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:237bec5dd1bfc9b40bbd786cd27949ef0c0eb5fab5eb491904c6b5df59d39d3c", size = 1642160 }, + { url = "https://files.pythonhosted.org/packages/a7/da/7091eef23291997e7c379a396eedbac66a50a145200cac86a0313286403d/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18123168cba156ab5794ea6de66db50f21bb3c66ae748d03316e71b27d907b95", size = 1664562 }, + { url = "https://files.pythonhosted.org/packages/bd/72/417ca8b5dde3c040c1cab1d5500fd24ffdf1a397cb86e36e958acb07cd65/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b75fe506c8e02769cc47f5ab21ce3e09b6211d3edaa8f8f27331cb6988779be", size = 3106304 }, + { url = "https://files.pythonhosted.org/packages/57/18/0877c12deb79cee67f6b8fbb662e2d038582d0e26d895ddbfdb88cea7e17/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da82aa4b46973aaf9e03bb4c3d6977004648c8638febfc0f9d237e865761270", size = 2302688 }, + { url = "https://files.pythonhosted.org/packages/c2/71/ca9e092c6d904f9fabadac6361e52a484165ee5970f34e4dc70a647f36f3/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c34c022d5ad564f1a5a57a4a89793bd70d7bad428150fb8ff2760b223407cdcf", size = 6893082 }, + { url = "https://files.pythonhosted.org/packages/c3/4c/99004b6ae04ead73d1e91829a78d9708c3c707aa83c1e782ea89f1ade491/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e96c84d6c2a0ca94e15acb5399118fff669f4306beb98a6d8ec6f5dccab4412", size = 2669909 }, + { url = "https://files.pythonhosted.org/packages/cb/7c/d5c93a0e497a0430b4f0bfea22e41317c22357cd557fa9aeeafb9e991d9b/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e8e154b84a311263e1aca86818c962e1fa9eefdd643d1d5d197fcd2738f88cb9", size = 3223759 }, + { url = "https://files.pythonhosted.org/packages/d6/77/2c22f438b643524b429731492665d91d9c654144e895f0051cee78d5928d/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:335fee93188f8cd585552bb8057228ce0111bd227fa81bfd40b7df6b75def8ab", size = 4148589 }, + { url = "https://files.pythonhosted.org/packages/bf/d3/51cc9f258b362fca8ced7c34046b66d8887551da0169c06c27ee8d2ce279/rapidfuzz-3.10.1-cp313-cp313-win32.whl", hash = "sha256:6729b856166a9e95c278410f73683957ea6100c8a9d0a8dbe434c49663689255", size = 1816180 }, + { url = "https://files.pythonhosted.org/packages/9d/9d/a69358047742dbc94516c71c07cfab4409d490578815c875949011e3f482/rapidfuzz-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:0e06d99ad1ad97cb2ef7f51ec6b1fedd74a3a700e4949353871cf331d07b382a", size = 1598626 }, + { url = "https://files.pythonhosted.org/packages/48/3c/8213b3216b542f3bd43051dc5a1c44e0cd741abb97bde064e89f241c5a82/rapidfuzz-3.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:8d1b7082104d596a3eb012e0549b2634ed15015b569f48879701e9d8db959dbb", size = 839138 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "retry" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/72/75d0b85443fbc8d9f38d08d2b1b67cc184ce35280e4a3813cda2f445f3a4/retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4", size = 6448 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "s3transfer" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/0a/1cdbabf9edd0ea7747efdf6c9ab4e7061b085aa7f9bfc36bb1601563b069/s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7", size = 145287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/05/7957af15543b8c9799209506df4660cba7afc4cf94bfb60513827e96bed6/s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", size = 83175 }, +] + +[[package]] +name = "safetensors" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/46/a1c56ed856c6ac3b1a8b37abe5be0cac53219367af1331e721b04d122577/safetensors-0.4.5.tar.gz", hash = "sha256:d73de19682deabb02524b3d5d1f8b3aaba94c72f1bbfc7911b9b9d5d391c0310", size = 65702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/a5/25bcf75e373412daf1fd88045ab3aa8140a0d804ef0e70712c4f2c5b94d8/safetensors-0.4.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:21f848d7aebd5954f92538552d6d75f7c1b4500f51664078b5b49720d180e47c", size = 392256 }, + { url = "https://files.pythonhosted.org/packages/08/8c/ece3bf8756506a890bd980eca02f47f9d98dfbf5ce16eda1368f53560f67/safetensors-0.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb07000b19d41e35eecef9a454f31a8b4718a185293f0d0b1c4b61d6e4487971", size = 381490 }, + { url = "https://files.pythonhosted.org/packages/39/83/c4a7ce01d626e46ea2b45887f2e59b16441408031e2ce2f9fe01860c6946/safetensors-0.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09dedf7c2fda934ee68143202acff6e9e8eb0ddeeb4cfc24182bef999efa9f42", size = 441093 }, + { url = "https://files.pythonhosted.org/packages/47/26/cc52de647e71bd9a0b0d78ead0d31d9c462b35550a817aa9e0cab51d6db4/safetensors-0.4.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59b77e4b7a708988d84f26de3ebead61ef1659c73dcbc9946c18f3b1786d2688", size = 438960 }, + { url = "https://files.pythonhosted.org/packages/06/78/332538546775ee97e749867df2d58f2282d9c48a1681e4891eed8b94ec94/safetensors-0.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d3bc83e14d67adc2e9387e511097f254bd1b43c3020440e708858c684cbac68", size = 478031 }, + { url = "https://files.pythonhosted.org/packages/d9/03/a3c8663f1ddda54e624ecf43fce651659b49e8e1603c52c3e464b442acfa/safetensors-0.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39371fc551c1072976073ab258c3119395294cf49cdc1f8476794627de3130df", size = 494754 }, + { url = "https://files.pythonhosted.org/packages/e6/ee/69e498a892f208bd1da4104d4b9be887f8611bf4942144718b6738482250/safetensors-0.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6c19feda32b931cae0acd42748a670bdf56bee6476a046af20181ad3fee4090", size = 435013 }, + { url = "https://files.pythonhosted.org/packages/a2/61/f0cfce984515b86d1260f556ba3b782158e2855e6a318446ac2613786fa9/safetensors-0.4.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a659467495de201e2f282063808a41170448c78bada1e62707b07a27b05e6943", size = 455984 }, + { url = "https://files.pythonhosted.org/packages/e7/a9/3e3b48fcaade3eb4e347d39ebf0bd44291db21a3e4507854b42a7cb910ac/safetensors-0.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bad5e4b2476949bcd638a89f71b6916fa9a5cae5c1ae7eede337aca2100435c0", size = 619513 }, + { url = "https://files.pythonhosted.org/packages/80/23/2a7a1be24258c0e44c1d356896fd63dc0545a98d2d0184925fa09cd3ec76/safetensors-0.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a3a315a6d0054bc6889a17f5668a73f94f7fe55121ff59e0a199e3519c08565f", size = 604841 }, + { url = "https://files.pythonhosted.org/packages/b4/5c/34d082ff1fffffd8545fb22cbae3285ab4236f1f0cfc64b7e58261c2363b/safetensors-0.4.5-cp311-none-win32.whl", hash = "sha256:a01e232e6d3d5cf8b1667bc3b657a77bdab73f0743c26c1d3c5dd7ce86bd3a92", size = 272602 }, + { url = "https://files.pythonhosted.org/packages/6d/41/948c96c8a7e9fef57c2e051f1871c108a6dbbc6d285598bdb1d89b98617c/safetensors-0.4.5-cp311-none-win_amd64.whl", hash = "sha256:cbd39cae1ad3e3ef6f63a6f07296b080c951f24cec60188378e43d3713000c04", size = 285973 }, + { url = "https://files.pythonhosted.org/packages/bf/ac/5a63082f931e99200db95fd46fb6734f050bb6e96bf02521904c6518b7aa/safetensors-0.4.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:473300314e026bd1043cef391bb16a8689453363381561b8a3e443870937cc1e", size = 392015 }, + { url = "https://files.pythonhosted.org/packages/73/95/ab32aa6e9bdc832ff87784cdf9da26192b93de3ef82b8d1ada8f345c5044/safetensors-0.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:801183a0f76dc647f51a2d9141ad341f9665602a7899a693207a82fb102cc53e", size = 381774 }, + { url = "https://files.pythonhosted.org/packages/d6/6c/7e04b7626809fc63f3698f4c50e43aff2864b40089aa4506c918a75b8eed/safetensors-0.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1524b54246e422ad6fb6aea1ac71edeeb77666efa67230e1faf6999df9b2e27f", size = 441134 }, + { url = "https://files.pythonhosted.org/packages/58/2b/ffe7c86a277e6c1595fbdf415cfe2903f253f574a5405e93fda8baaa582c/safetensors-0.4.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3139098e3e8b2ad7afbca96d30ad29157b50c90861084e69fcb80dec7430461", size = 438467 }, + { url = "https://files.pythonhosted.org/packages/67/9c/f271bd804e08c7fda954d17b70ff281228a88077337a9e70feace4f4cc93/safetensors-0.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65573dc35be9059770808e276b017256fa30058802c29e1038eb1c00028502ea", size = 476566 }, + { url = "https://files.pythonhosted.org/packages/4c/ad/4cf76a3e430a8a26108407fa6cb93e6f80d996a5cb75d9540c8fe3862990/safetensors-0.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd33da8e9407559f8779c82a0448e2133737f922d71f884da27184549416bfed", size = 492253 }, + { url = "https://files.pythonhosted.org/packages/d9/40/a6f75ea449a9647423ec8b6f72c16998d35aa4b43cb38536ac060c5c7bf5/safetensors-0.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3685ce7ed036f916316b567152482b7e959dc754fcc4a8342333d222e05f407c", size = 434769 }, + { url = "https://files.pythonhosted.org/packages/52/47/d4b49b1231abf3131f7bb0bc60ebb94b27ee33e0a1f9569da05f8ac65dee/safetensors-0.4.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dde2bf390d25f67908278d6f5d59e46211ef98e44108727084d4637ee70ab4f1", size = 457166 }, + { url = "https://files.pythonhosted.org/packages/c3/cd/006468b03b0fa42ff82d795d47c4193e99001e96c3f08bd62ef1b5cab586/safetensors-0.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7469d70d3de970b1698d47c11ebbf296a308702cbaae7fcb993944751cf985f4", size = 619280 }, + { url = "https://files.pythonhosted.org/packages/22/4d/b6208d918e83daa84b424c0ac3191ae61b44b3191613a3a5a7b38f94b8ad/safetensors-0.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a6ba28118636a130ccbb968bc33d4684c48678695dba2590169d5ab03a45646", size = 605390 }, + { url = "https://files.pythonhosted.org/packages/e8/20/bf0e01825dc01ed75538021a98b9a046e60ead63c6c6700764c821a8c873/safetensors-0.4.5-cp312-none-win32.whl", hash = "sha256:c859c7ed90b0047f58ee27751c8e56951452ed36a67afee1b0a87847d065eec6", size = 273250 }, + { url = "https://files.pythonhosted.org/packages/f1/5f/ab6b6cec85b40789801f35b7d2fb579ae242d8193929974a106d5ff5c835/safetensors-0.4.5-cp312-none-win_amd64.whl", hash = "sha256:b5a8810ad6a6f933fff6c276eae92c1da217b39b4d8b1bc1c0b8af2d270dc532", size = 286307 }, + { url = "https://files.pythonhosted.org/packages/90/61/0e27b1403e311cba0be20026bee4ee822d90eda7dad372179e7f18bb99f3/safetensors-0.4.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:25e5f8e2e92a74f05b4ca55686234c32aac19927903792b30ee6d7bd5653d54e", size = 392062 }, + { url = "https://files.pythonhosted.org/packages/b1/9f/cc31fafc9f5d79da10a83a820ca37f069bab0717895ad8cbcacf629dd1c5/safetensors-0.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81efb124b58af39fcd684254c645e35692fea81c51627259cdf6d67ff4458916", size = 382517 }, + { url = "https://files.pythonhosted.org/packages/a4/c7/4fda8a0ebb96662550433378f4a74c677fa5fc4d0a43a7ec287d1df254a9/safetensors-0.4.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:585f1703a518b437f5103aa9cf70e9bd437cb78eea9c51024329e4fb8a3e3679", size = 441378 }, + { url = "https://files.pythonhosted.org/packages/14/31/9abb431f6209de9c80dab83e1112ebd769f1e32e7ab7ab228a02424a4693/safetensors-0.4.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b99fbf72e3faf0b2f5f16e5e3458b93b7d0a83984fe8d5364c60aa169f2da89", size = 438831 }, + { url = "https://files.pythonhosted.org/packages/37/37/99bfb195578a808b8d045159ee9264f8da58d017ac0701853dcacda14d4e/safetensors-0.4.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b17b299ca9966ca983ecda1c0791a3f07f9ca6ab5ded8ef3d283fff45f6bcd5f", size = 477112 }, + { url = "https://files.pythonhosted.org/packages/7d/05/fac3ef107e60d2a78532bed171a91669d4bb259e1236f5ea8c67a6976c75/safetensors-0.4.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76ded72f69209c9780fdb23ea89e56d35c54ae6abcdec67ccb22af8e696e449a", size = 493373 }, + { url = "https://files.pythonhosted.org/packages/cf/7a/825800ee8c68214b4fd3506d5e19209338c69b41e01c6e14dd13969cc8b9/safetensors-0.4.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2783956926303dcfeb1de91a4d1204cd4089ab441e622e7caee0642281109db3", size = 435422 }, + { url = "https://files.pythonhosted.org/packages/5e/6c/7a3233c08bde558d6c33a41219119866cb596139a4673cc6c24024710ffd/safetensors-0.4.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d94581aab8c6b204def4d7320f07534d6ee34cd4855688004a4354e63b639a35", size = 457382 }, + { url = "https://files.pythonhosted.org/packages/a0/58/0b7bcba3788ff503990cf9278d611b56c029400612ba93e772c987b5aa03/safetensors-0.4.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:67e1e7cb8678bb1b37ac48ec0df04faf689e2f4e9e81e566b5c63d9f23748523", size = 619301 }, + { url = "https://files.pythonhosted.org/packages/82/cc/9c2cf58611daf1c83ce5d37f9de66353e23fcda36008b13fd3409a760aa3/safetensors-0.4.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbd280b07e6054ea68b0cb4b16ad9703e7d63cd6890f577cb98acc5354780142", size = 605580 }, +] + +[[package]] +name = "scalecodec" +version = "1.2.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "base58" }, + { name = "more-itertools" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/7c/703893e7a8751318517a3dd8c0c060b2c30ffa33f4ab5dd6a4ed483f7967/scalecodec-1.2.11.tar.gz", hash = "sha256:99a2cdbfccdcaf22bd86b86da55a730a2855514ad2309faef4a4a93ac6cbeb8d", size = 150260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/60/2a903fa9ed3dfc842240da22969a25b16ea213ed3ee25b7ba8ae1cba20c7/scalecodec-1.2.11-py3-none-any.whl", hash = "sha256:d15c94965f617caa25096f83a45f5f73031d05e6ee08d6039969f0a64fc35de1", size = 99164 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/23/643a9958d1d14f5ba1f0204396d5953f926624b3f95b77af7904fb406d03/sentry_sdk-2.19.1.tar.gz", hash = "sha256:6ad8507457a379b72f832aca55787b21e7391751892faef1fd8bace350aa5e17", size = 298915 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/b1/b03f54c8b379d493bd19f9dca241efdd17f77a8f7a34b80c2d4417dfc7b7/sentry_sdk-2.19.1-py2.py3-none-any.whl", hash = "sha256:b056e04b766f805fdf0aa620482cafe2ff000c8fcb51cb266cdb90873e93837b", size = 322816 }, +] + +[[package]] +name = "setproctitle" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/4e/b09341b19b9ceb8b4c67298ab4a08ef7a4abdd3016c7bb152e9b6379031d/setproctitle-1.3.4.tar.gz", hash = "sha256:3b40d32a3e1f04e94231ed6dfee0da9e43b4f9c6b5450d53e6dd7754c34e0c50", size = 26456 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/1a/1fb7d622195bcb3ce7b04366a833e51cfa5ad632c5dafe32e0763cd3fdc9/setproctitle-1.3.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0f749f07002c2d6fecf37cedc43207a88e6c651926a470a5f229070cf791879", size = 16851 }, + { url = "https://files.pythonhosted.org/packages/46/54/e3aa4f46eddf795f10452ea878ff85c3496d36409636530f9a37e2de3cbe/setproctitle-1.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90ea8d302a5d30b948451d146e94674a3c5b020cc0ced9a1c28f8ddb0f203a5d", size = 11620 }, + { url = "https://files.pythonhosted.org/packages/61/47/80988221679dfd93c464248abb71c2a96338f2ca3f8e3288d0ecb7422f4d/setproctitle-1.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f859c88193ed466bee4eb9d45fbc29d2253e6aa3ccd9119c9a1d8d95f409a60d", size = 31519 }, + { url = "https://files.pythonhosted.org/packages/2c/72/14984c127f708597e412f1a8cf7cac809b9bca50a267a6b01b221b094330/setproctitle-1.3.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3afa5a0ed08a477ded239c05db14c19af585975194a00adf594d48533b23701", size = 32860 }, + { url = "https://files.pythonhosted.org/packages/16/9d/34ea09295620fddae65cf7caeac81bbfc386a3ae6ce26a4dcadbb54c134d/setproctitle-1.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a78fce9018cc3e9a772b6537bbe3fe92380acf656c9f86db2f45e685af376e", size = 30029 }, + { url = "https://files.pythonhosted.org/packages/44/bf/a447a51054ceed23f69d4f7370289044b4508569f11da6db2eec087bc174/setproctitle-1.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d758e2eed2643afac5f2881542fbb5aa97640b54be20d0a5ed0691d02f0867d", size = 31017 }, + { url = "https://files.pythonhosted.org/packages/ec/46/adcffde6fb8d95458da0a568afdf0dabbbff6470299d94014676e1ab43c0/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef133a1a2ee378d549048a12d56f4ef0e2b9113b0b25b6b77821e9af94d50634", size = 30762 }, + { url = "https://files.pythonhosted.org/packages/a3/cd/747a67ce1f6ef8fd1fa46b0b13ba0e007b80914bd549318830b8691ab9f6/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d2a154b79d5fb42d1eff06e05e22f0e8091261d877dd47b37d31352b74ecc37", size = 29753 }, + { url = "https://files.pythonhosted.org/packages/3d/86/5939546e57238462a7839ae78399a635d1cfc5d125c7a12a28face111730/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:202eae632815571297833876a0f407d0d9c7ad9d843b38adbe687fe68c5192ee", size = 32161 }, + { url = "https://files.pythonhosted.org/packages/62/83/9194a4baed06e0e90a69e2e4a77a75e5a3ff008046870c79bc36a5c45e1c/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2b0080819859e80a7776ac47cf6accb4b7ad313baf55fabac89c000480dcd103", size = 30104 }, + { url = "https://files.pythonhosted.org/packages/ac/cd/08928fec23cbf4dae2a7b245b72d86e6458d64f4e7e6956cd80a9fda8c80/setproctitle-1.3.4-cp311-cp311-win32.whl", hash = "sha256:9c9d7d1267dee8c6627963d9376efa068858cfc8f573c083b1b6a2d297a8710f", size = 11349 }, + { url = "https://files.pythonhosted.org/packages/aa/19/240c4b99d57e045d3b2e2effa5924e810eabb18c56ef9c2336a7746dffe4/setproctitle-1.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:475986ddf6df65d619acd52188336a20f616589403f5a5ceb3fc70cdc137037a", size = 12071 }, + { url = "https://files.pythonhosted.org/packages/94/1f/02fb3c6038c819d86765316d2a911281fc56c7dd3a9355dceb3f26a5bf7b/setproctitle-1.3.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d06990dcfcd41bb3543c18dd25c8476fbfe1f236757f42fef560f6aa03ac8dfc", size = 16842 }, + { url = "https://files.pythonhosted.org/packages/b8/0c/d69e1f91c8f3d3aa74394e9e6ebb818f7d323e2d138ce1127e9462d09ebc/setproctitle-1.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:317218c9d8b17a010ab2d2f0851e8ef584077a38b1ba2b7c55c9e44e79a61e73", size = 11614 }, + { url = "https://files.pythonhosted.org/packages/86/ed/8031871d275302054b2f1b94b7cf5e850212cc412fe968f0979e64c1b838/setproctitle-1.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb5fefb53b9d9f334a5d9ec518a36b92a10b936011ac8a6b6dffd60135f16459", size = 31840 }, + { url = "https://files.pythonhosted.org/packages/45/b7/04f5d221cbdcff35d6cdf74e2a852e69dc8d8e746eb1b314be6b57b79c41/setproctitle-1.3.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0855006261635e8669646c7c304b494b6df0a194d2626683520103153ad63cc9", size = 33271 }, + { url = "https://files.pythonhosted.org/packages/25/b2/8dff0d2a72076e5535f117f33458d520538b5a0900b90a9f59a278f0d3f6/setproctitle-1.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a88e466fcaee659679c1d64dcb2eddbcb4bfadffeb68ba834d9c173a25b6184", size = 30509 }, + { url = "https://files.pythonhosted.org/packages/4b/cf/4f19cdc7fdff3eaeb3064ce6eeb27c63081dba3123fbf904ac6bf0de440c/setproctitle-1.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f963b6ed8ba33eda374a98d979e8a0eaf21f891b6e334701693a2c9510613c4c", size = 31543 }, + { url = "https://files.pythonhosted.org/packages/9b/a7/5f9c3c70dc5573f660f978fb3bb4847cd26ede95a5fc294d3f1cf6779800/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:122c2e05697fa91f5d23f00bbe98a9da1bd457b32529192e934095fadb0853f1", size = 31268 }, + { url = "https://files.pythonhosted.org/packages/26/ab/bbde90ea0ed6a062ef94fe1c609b68077f7eb586133a62fa62d0c8dd9f8c/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1bba0a866f5895d5b769d8c36b161271c7fd407e5065862ab80ff91c29fbe554", size = 30232 }, + { url = "https://files.pythonhosted.org/packages/36/0e/817be9934eda4cf63c96c694c3383cb0d2e5d019a2871af7dbd2202f7a58/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97f1f861998e326e640708488c442519ad69046374b2c3fe9bcc9869b387f23c", size = 32739 }, + { url = "https://files.pythonhosted.org/packages/b0/76/9b4877850c9c5f41c4bacae441285dead7c192bebf4fcbf3b3eb0e8033cc/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:726aee40357d4bdb70115442cb85ccc8e8bc554fc0bbbaa3a57cbe81df42287d", size = 30778 }, + { url = "https://files.pythonhosted.org/packages/b2/fa/bbc7ab32f253b9700ac20d78ba0d5fbdc4ea5789d33e1adb236cdf20b23a/setproctitle-1.3.4-cp312-cp312-win32.whl", hash = "sha256:04d6ba8b816dbb0bfd62000b0c3e583160893e6e8c4233e1dca1a9ae4d95d924", size = 11355 }, + { url = "https://files.pythonhosted.org/packages/44/5c/6e6665b5fd800206a9e537ab0d2630d7b9b31b4697d931ed468837cc9cf5/setproctitle-1.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:9c76e43cb351ba8887371240b599925cdf3ecececc5dfb7125c71678e7722c55", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/d4/01/51d07ab1dbec8885ebad419d254c06b9e28f4363c163b737a89995a52b75/setproctitle-1.3.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6e3b177e634aa6bbbfbf66d097b6d1cdb80fc60e912c7d8bace2e45699c07dd", size = 16831 }, + { url = "https://files.pythonhosted.org/packages/30/03/deff7089b525c0d8ec047e06661d2be67c87685a99be6a6aed2890b81c8f/setproctitle-1.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b17655a5f245b416e127e02087ea6347a48821cc4626bc0fd57101bfcd88afc", size = 11607 }, + { url = "https://files.pythonhosted.org/packages/ea/be/cb2950b3f6ba460f530bda2c713828236c75d982d0aa0f62b33429a9b4d0/setproctitle-1.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa5057a86df920faab8ee83960b724bace01a3231eb8e3f2c93d78283504d598", size = 31881 }, + { url = "https://files.pythonhosted.org/packages/5c/b4/1f0dba7525a2fbefd08d4086e7e998d9c7581153807fb6b3083d06e0b8e2/setproctitle-1.3.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149fdfb8a26a555780c4ce53c92e6d3c990ef7b30f90a675eca02e83c6d5f76d", size = 33290 }, + { url = "https://files.pythonhosted.org/packages/2d/a8/07a160f9dcd1a7b1cad39ce6cbaf4425837502b0592a400c38cb21f0f247/setproctitle-1.3.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ded03546938a987f463c68ab98d683af87a83db7ac8093bbc179e77680be5ba2", size = 30489 }, + { url = "https://files.pythonhosted.org/packages/83/0c/3d972d9ea4165961a9764df5324d42bf2d059cb8a6ef516c67f068ed4d92/setproctitle-1.3.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ab9f5b7f2bbc1754bc6292d9a7312071058e5a891b0391e6d13b226133f36aa", size = 31576 }, + { url = "https://files.pythonhosted.org/packages/7a/c0/c12bdc2c91009defdd1b207ff156ccd691f5b9a6a0aae1ed9126d4ff9a0c/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b19813c852566fa031902124336fa1f080c51e262fc90266a8c3d65ca47b74c", size = 31273 }, + { url = "https://files.pythonhosted.org/packages/4f/83/8d704bee57990b27537adf7c97540f32226ffa3922fb26bdd459da8a4470/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db78b645dc63c0ccffca367a498f3b13492fb106a2243a1e998303ba79c996e2", size = 30236 }, + { url = "https://files.pythonhosted.org/packages/d8/42/94e31d1f515f831e1ae43f2405794257eb940a7972b2fbb6283790db2958/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b669aaac70bd9f03c070270b953f78d9ee56c4af6f0ff9f9cd3e6d1878c10b40", size = 32766 }, + { url = "https://files.pythonhosted.org/packages/83/53/01746ed8fb75239a001ee89d5eb8ad5a3022df510572d1cf60dd04567e13/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6dc3d656702791565994e64035a208be56b065675a5bc87b644c657d6d9e2232", size = 30812 }, + { url = "https://files.pythonhosted.org/packages/5f/ea/3ce61e70a6b898e95c0a1e393964c829103dc4ad4b0732cd70c8fc13e54c/setproctitle-1.3.4-cp313-cp313-win32.whl", hash = "sha256:091f682809a4d12291cf0205517619d2e7014986b7b00ebecfde3d76f8ae5a8f", size = 11349 }, + { url = "https://files.pythonhosted.org/packages/e7/1a/8149da1c19db6bd57164d62b1d91c188e7d77e695947cf1ac327c8aea513/setproctitle-1.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:adcd6ba863a315702184d92d3d3bbff290514f24a14695d310f02ae5e28bd1f7", size = 12062 }, +] + +[[package]] +name = "setuptools" +version = "70.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/60/5db2249526c9b453c5bb8b9f6965fcab0ddb7f40ad734420b3b421f7da44/setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0", size = 2265182 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/88/70c5767a0e43eb4451c2200f07d042a4bcd7639276003a9c54a68cfcc1f8/setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", size = 863432 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smmap" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/04/b5bf6d21dc4041000ccba7eb17dd3055feb237e7ffc2c20d3fae3af62baa/smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", size = 22291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a5/10f97f73544edcdef54409f1d839f6049a0d79df68adbc1ceb24d1aaca42/smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da", size = 24282 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/b5/6bceb93ff20bd7ca36e6f7c540581abb18f53130fabb30ba526e26fd819b/starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823", size = 2843736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/18/31fa32ed6c68ba66220204ef0be798c349d0a20c1901f9d4a794e08c76d8/starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee", size = 71908 }, +] + +[[package]] +name = "substrate-interface" +version = "1.7.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "base58" }, + { name = "certifi" }, + { name = "ecdsa" }, + { name = "eth-keys" }, + { name = "eth-utils" }, + { name = "idna" }, + { name = "py-bip39-bindings" }, + { name = "py-ed25519-zebra-bindings" }, + { name = "py-sr25519-bindings" }, + { name = "pycryptodome" }, + { name = "pynacl" }, + { name = "requests" }, + { name = "scalecodec" }, + { name = "websocket-client" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/44/825433c906bdb69ab66fd3967c11fcfbcd953241e9d6257fd6a21c4cdc76/substrate-interface-1.7.11.tar.gz", hash = "sha256:4caa5eacb9996edbe76ad12249521b3542bbd8d9d69b96734087201db1fef8f6", size = 79221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/e1/37344b7acd260cbfed13563dcbab391c7c4b0c9eca5ec59aba138c5dca9e/substrate_interface-1.7.11-py3-none-any.whl", hash = "sha256:ce19bc97481769238ed23c752db985a3058637918693f2db6aeed2fab3756075", size = 60273 }, +] + +[[package]] +name = "sympy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 }, +] + +[[package]] +name = "termcolor" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, +] + +[[package]] +name = "tokenizers" +version = "0.20.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/25/b1681c1c30ea3ea6e584ae3fffd552430b12faa599b558c4c4783f56d7ff/tokenizers-0.20.3.tar.gz", hash = "sha256:2278b34c5d0dd78e087e1ca7f9b1dcbf129d80211afa645f214bd6e051037539", size = 340513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/93/6742ef9206409d5ce1fdf44d5ca1687cdc3847ba0485424e2c731e6bcf67/tokenizers-0.20.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:585b51e06ca1f4839ce7759941e66766d7b060dccfdc57c4ca1e5b9a33013a90", size = 2674224 }, + { url = "https://files.pythonhosted.org/packages/aa/14/e75ece72e99f6ef9ae07777ca9fdd78608f69466a5cecf636e9bd2f25d5c/tokenizers-0.20.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61cbf11954f3b481d08723ebd048ba4b11e582986f9be74d2c3bdd9293a4538d", size = 2558991 }, + { url = "https://files.pythonhosted.org/packages/46/54/033b5b2ba0c3ae01e026c6f7ced147d41a2fa1c573d00a66cb97f6d7f9b3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef820880d5e4e8484e2fa54ff8d297bb32519eaa7815694dc835ace9130a3eea", size = 2892476 }, + { url = "https://files.pythonhosted.org/packages/e6/b0/cc369fb3297d61f3311cab523d16d48c869dc2f0ba32985dbf03ff811041/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67ef4dcb8841a4988cd00dd288fb95dfc8e22ed021f01f37348fd51c2b055ba9", size = 2802775 }, + { url = "https://files.pythonhosted.org/packages/1a/74/62ad983e8ea6a63e04ed9c5be0b605056bf8aac2f0125f9b5e0b3e2b89fa/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff1ef8bd47a02b0dc191688ccb4da53600df5d4c9a05a4b68e1e3de4823e78eb", size = 3086138 }, + { url = "https://files.pythonhosted.org/packages/6b/ac/4637ba619db25094998523f9e6f5b456e1db1f8faa770a3d925d436db0c3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:444d188186eab3148baf0615b522461b41b1f0cd58cd57b862ec94b6ac9780f1", size = 3098076 }, + { url = "https://files.pythonhosted.org/packages/58/ce/9793f2dc2ce529369807c9c74e42722b05034af411d60f5730b720388c7d/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37c04c032c1442740b2c2d925f1857885c07619224a533123ac7ea71ca5713da", size = 3379650 }, + { url = "https://files.pythonhosted.org/packages/50/f6/2841de926bc4118af996eaf0bdf0ea5b012245044766ffc0347e6c968e63/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453c7769d22231960ee0e883d1005c93c68015025a5e4ae56275406d94a3c907", size = 2994005 }, + { url = "https://files.pythonhosted.org/packages/a3/b2/00915c4fed08e9505d37cf6eaab45b12b4bff8f6719d459abcb9ead86a4b/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4bb31f7b2847e439766aaa9cc7bccf7ac7088052deccdb2275c952d96f691c6a", size = 8977488 }, + { url = "https://files.pythonhosted.org/packages/e9/ac/1c069e7808181ff57bcf2d39e9b6fbee9133a55410e6ebdaa89f67c32e83/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:843729bf0f991b29655a069a2ff58a4c24375a553c70955e15e37a90dd4e045c", size = 9294935 }, + { url = "https://files.pythonhosted.org/packages/50/47/722feb70ee68d1c4412b12d0ea4acc2713179fd63f054913990f9e259492/tokenizers-0.20.3-cp311-none-win32.whl", hash = "sha256:efcce3a927b1e20ca694ba13f7a68c59b0bd859ef71e441db68ee42cf20c2442", size = 2197175 }, + { url = "https://files.pythonhosted.org/packages/75/68/1b4f928b15a36ed278332ac75d66d7eb65d865bf344d049c452c18447bf9/tokenizers-0.20.3-cp311-none-win_amd64.whl", hash = "sha256:88301aa0801f225725b6df5dea3d77c80365ff2362ca7e252583f2b4809c4cc0", size = 2381616 }, + { url = "https://files.pythonhosted.org/packages/07/00/92a08af2a6b0c88c50f1ab47d7189e695722ad9714b0ee78ea5e1e2e1def/tokenizers-0.20.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:49d12a32e190fad0e79e5bdb788d05da2f20d8e006b13a70859ac47fecf6ab2f", size = 2667951 }, + { url = "https://files.pythonhosted.org/packages/ec/9a/e17a352f0bffbf415cf7d73756f5c73a3219225fc5957bc2f39d52c61684/tokenizers-0.20.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:282848cacfb9c06d5e51489f38ec5aa0b3cd1e247a023061945f71f41d949d73", size = 2555167 }, + { url = "https://files.pythonhosted.org/packages/27/37/d108df55daf4f0fcf1f58554692ff71687c273d870a34693066f0847be96/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe4e08c7d0cd6154c795deb5bf81d2122f36daf075e0c12a8b050d824ef0a64", size = 2898389 }, + { url = "https://files.pythonhosted.org/packages/b2/27/32f29da16d28f59472fa7fb38e7782069748c7e9ab9854522db20341624c/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca94fc1b73b3883c98f0c88c77700b13d55b49f1071dfd57df2b06f3ff7afd64", size = 2795866 }, + { url = "https://files.pythonhosted.org/packages/29/4e/8a9a3c89e128c4a40f247b501c10279d2d7ade685953407c4d94c8c0f7a7/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef279c7e239f95c8bdd6ff319d9870f30f0d24915b04895f55b1adcf96d6c60d", size = 3085446 }, + { url = "https://files.pythonhosted.org/packages/b4/3b/a2a7962c496ebcd95860ca99e423254f760f382cd4bd376f8895783afaf5/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16384073973f6ccbde9852157a4fdfe632bb65208139c9d0c0bd0176a71fd67f", size = 3094378 }, + { url = "https://files.pythonhosted.org/packages/1f/f4/a8a33f0192a1629a3bd0afcad17d4d221bbf9276da4b95d226364208d5eb/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:312d522caeb8a1a42ebdec87118d99b22667782b67898a76c963c058a7e41d4f", size = 3385755 }, + { url = "https://files.pythonhosted.org/packages/9e/65/c83cb3545a65a9eaa2e13b22c93d5e00bd7624b354a44adbdc93d5d9bd91/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b7cb962564785a83dafbba0144ecb7f579f1d57d8c406cdaa7f32fe32f18ad", size = 2997679 }, + { url = "https://files.pythonhosted.org/packages/55/e9/a80d4e592307688a67c7c59ab77e03687b6a8bd92eb5db763a2c80f93f57/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:124c5882ebb88dadae1fc788a582299fcd3a8bd84fc3e260b9918cf28b8751f5", size = 8989296 }, + { url = "https://files.pythonhosted.org/packages/90/af/60c957af8d2244321124e893828f1a4817cde1a2d08d09d423b73f19bd2f/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2b6e54e71f84c4202111a489879005cb14b92616a87417f6c102c833af961ea2", size = 9303621 }, + { url = "https://files.pythonhosted.org/packages/be/a9/96172310ee141009646d63a1ca267c099c462d747fe5ef7e33f74e27a683/tokenizers-0.20.3-cp312-none-win32.whl", hash = "sha256:83d9bfbe9af86f2d9df4833c22e94d94750f1d0cd9bfb22a7bb90a86f61cdb1c", size = 2188979 }, + { url = "https://files.pythonhosted.org/packages/bd/68/61d85ae7ae96dde7d0974ff3538db75d5cdc29be2e4329cd7fc51a283e22/tokenizers-0.20.3-cp312-none-win_amd64.whl", hash = "sha256:44def74cee574d609a36e17c8914311d1b5dbcfe37c55fd29369d42591b91cf2", size = 2380725 }, + { url = "https://files.pythonhosted.org/packages/07/19/36e9eaafb229616cb8502b42030fa7fe347550e76cb618de71b498fc3222/tokenizers-0.20.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0b630e0b536ef0e3c8b42c685c1bc93bd19e98c0f1543db52911f8ede42cf84", size = 2666813 }, + { url = "https://files.pythonhosted.org/packages/b9/c7/e2ce1d4f756c8a62ef93fdb4df877c2185339b6d63667b015bf70ea9d34b/tokenizers-0.20.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a02d160d2b19bcbfdf28bd9a4bf11be4cb97d0499c000d95d4c4b1a4312740b6", size = 2555354 }, + { url = "https://files.pythonhosted.org/packages/7c/cf/5309c2d173a6a67f9ec8697d8e710ea32418de6fd8541778032c202a1c3e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e3d80d89b068bc30034034b5319218c7c0a91b00af19679833f55f3becb6945", size = 2897745 }, + { url = "https://files.pythonhosted.org/packages/2c/e5/af3078e32f225e680e69d61f78855880edb8d53f5850a1834d519b2b103f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:174a54910bed1b089226512b4458ea60d6d6fd93060254734d3bc3540953c51c", size = 2794385 }, + { url = "https://files.pythonhosted.org/packages/0b/a7/bc421fe46650cc4eb4a913a236b88c243204f32c7480684d2f138925899e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:098b8a632b8656aa5802c46689462c5c48f02510f24029d71c208ec2c822e771", size = 3084580 }, + { url = "https://files.pythonhosted.org/packages/c6/22/97e1e95ee81f75922c9f569c23cb2b1fdc7f5a7a29c4c9fae17e63f751a6/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78c8c143e3ae41e718588281eb3e212c2b31623c9d6d40410ec464d7d6221fb5", size = 3093581 }, + { url = "https://files.pythonhosted.org/packages/d5/14/f0df0ee3b9e516121e23c0099bccd7b9f086ba9150021a750e99b16ce56f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b26b0aadb18cd8701077362ba359a06683662d5cafe3e8e8aba10eb05c037f1", size = 3385934 }, + { url = "https://files.pythonhosted.org/packages/66/52/7a171bd4929e3ffe61a29b4340fe5b73484709f92a8162a18946e124c34c/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07d7851a72717321022f3774e84aa9d595a041d643fafa2e87fbc9b18711dac0", size = 2997311 }, + { url = "https://files.pythonhosted.org/packages/7c/64/f1993bb8ebf775d56875ca0d50a50f2648bfbbb143da92fe2e6ceeb4abd5/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bd44e48a430ada902c6266a8245f5036c4fe744fcb51f699999fbe82aa438797", size = 8988601 }, + { url = "https://files.pythonhosted.org/packages/d6/3f/49fa63422159bbc2f2a4ac5bfc597d04d4ec0ad3d2ef46649b5e9a340e37/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a4c186bb006ccbe1f5cc4e0380d1ce7806f5955c244074fd96abc55e27b77f01", size = 9303950 }, + { url = "https://files.pythonhosted.org/packages/66/11/79d91aeb2817ad1993ef61c690afe73e6dbedbfb21918b302ef5a2ba9bfb/tokenizers-0.20.3-cp313-none-win32.whl", hash = "sha256:6e19e0f1d854d6ab7ea0c743d06e764d1d9a546932be0a67f33087645f00fe13", size = 2188941 }, + { url = "https://files.pythonhosted.org/packages/c2/ff/ac8410f868fb8b14b5e619efa304aa119cb8a40bd7df29fc81a898e64f99/tokenizers-0.20.3-cp313-none-win_amd64.whl", hash = "sha256:d50ede425c7e60966a9680d41b58b3a0950afa1bb570488e2972fa61662c4273", size = 2380269 }, +] + +[[package]] +name = "toml" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/19/5cbd78eac8b1783671c40e34bb0fa83133a06d340a38b55c645076d40094/toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", size = 16719 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/12/ced7105d2de62fa7c8fb5fce92cc4ce66b57c95fb875e9318dba7f8c5db0/toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", size = 25796 }, +] + +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383 }, +] + +[[package]] +name = "torch" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/35/e8b2daf02ce933e4518e6f5682c72fd0ed66c15910ea1fb4168f442b71c4/torch-2.5.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:de5b7d6740c4b636ef4db92be922f0edc425b65ed78c5076c43c42d362a45457", size = 906474467 }, + { url = "https://files.pythonhosted.org/packages/40/04/bd91593a4ca178ece93ca55f27e2783aa524aaccbfda66831d59a054c31e/torch-2.5.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:340ce0432cad0d37f5a31be666896e16788f1adf8ad7be481196b503dad675b9", size = 91919450 }, + { url = "https://files.pythonhosted.org/packages/0d/4a/e51420d46cfc90562e85af2fee912237c662ab31140ab179e49bd69401d6/torch-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:603c52d2fe06433c18b747d25f5c333f9c1d58615620578c326d66f258686f9a", size = 203098237 }, + { url = "https://files.pythonhosted.org/packages/d0/db/5d9cbfbc7968d79c5c09a0bc0bc3735da079f2fd07cc10498a62b320a480/torch-2.5.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:31f8c39660962f9ae4eeec995e3049b5492eb7360dd4f07377658ef4d728fa4c", size = 63884466 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/36c114d120bfe10f9323ed35061bc5878cc74f3f594003854b0ea298942f/torch-2.5.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:ed231a4b3a5952177fafb661213d690a72caaad97d5824dd4fc17ab9e15cec03", size = 906389343 }, + { url = "https://files.pythonhosted.org/packages/6d/69/d8ada8b6e0a4257556d5b4ddeb4345ea8eeaaef3c98b60d1cca197c7ad8e/torch-2.5.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:3f4b7f10a247e0dcd7ea97dc2d3bfbfc90302ed36d7f3952b0008d0df264e697", size = 91811673 }, + { url = "https://files.pythonhosted.org/packages/5f/ba/607d013b55b9fd805db2a5c2662ec7551f1910b4eef39653eeaba182c5b2/torch-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:73e58e78f7d220917c5dbfad1a40e09df9929d3b95d25e57d9f8558f84c9a11c", size = 203046841 }, + { url = "https://files.pythonhosted.org/packages/57/6c/bf52ff061da33deb9f94f4121fde7ff3058812cb7d2036c97bc167793bd1/torch-2.5.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:8c712df61101964eb11910a846514011f0b6f5920c55dbf567bff8a34163d5b1", size = 63858109 }, + { url = "https://files.pythonhosted.org/packages/69/72/20cb30f3b39a9face296491a86adb6ff8f1a47a897e4d14667e6cf89d5c3/torch-2.5.1-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:9b61edf3b4f6e3b0e0adda8b3960266b9009d02b37555971f4d1c8f7a05afed7", size = 906393265 }, +] + +[[package]] +name = "tplr" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "aioboto3" }, + { name = "aiobotocore" }, + { name = "aiofiles" }, + { name = "bittensor" }, + { name = "boto3" }, + { name = "bt-decode" }, + { name = "einops" }, + { name = "pip" }, + { name = "python-dotenv" }, + { name = "torch" }, + { name = "transformers" }, + { name = "wandb" }, +] + +[package.metadata] +requires-dist = [ + { name = "aioboto3" }, + { name = "aiobotocore" }, + { name = "aiofiles" }, + { name = "bittensor", specifier = "==8.5.1" }, + { name = "boto3" }, + { name = "bt-decode", specifier = "==0.4.0" }, + { name = "einops" }, + { name = "pip" }, + { name = "python-dotenv" }, + { name = "torch" }, + { name = "transformers" }, + { name = "wandb" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "transformers" +version = "4.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/5a/58f96c83e566f907ae39f16d4401bbefd8bb85c60bd1e6a95c419752ab90/transformers-4.46.3.tar.gz", hash = "sha256:8ee4b3ae943fe33e82afff8e837f4b052058b07ca9be3cb5b729ed31295f72cc", size = 8627944 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/51/b87caa939fedf307496e4dbf412f4b909af3d9ca8b189fc3b65c1faa456f/transformers-4.46.3-py3-none-any.whl", hash = "sha256:a12ef6f52841fd190a3e5602145b542d03507222f2c64ebb7ee92e8788093aef", size = 10034536 }, +] + +[[package]] +name = "triton" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/17/d9a5cf4fcf46291856d1e90762e36cbabd2a56c7265da0d1d9508c8e3943/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f34f6e7885d1bf0eaaf7ba875a5f0ce6f3c13ba98f9503651c1e6dc6757ed5c", size = 209506424 }, + { url = "https://files.pythonhosted.org/packages/78/eb/65f5ba83c2a123f6498a3097746607e5b2f16add29e36765305e4ac7fdd8/triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc", size = 209551444 }, +] + +[[package]] +name = "typer" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "uvicorn" +version = "0.32.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 }, +] + +[[package]] +name = "wandb" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "docker-pycreds" }, + { name = "gitpython" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "setproctitle" }, + { name = "setuptools" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/cc/3322f2c4d85b84a18cb93e97ecad216fe6a59ec39118a82bdfed7872f660/wandb-0.19.0.tar.gz", hash = "sha256:cfacf2cc323561909e7572e772a4a5f849f28248a4529247b199466171cd84f8", size = 11821728 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/a0/5d8f6268a728fbf008797967587dfad29cd4837a1e3ea33ad2ded074a0a0/wandb-0.19.0-py3-none-any.whl", hash = "sha256:d4dab974f8fd5304ae5af961777d89ba4622d776b18882dc091098a7eace6ca3", size = 6247909 }, + { url = "https://files.pythonhosted.org/packages/3b/23/0e60bee6cf1e738e34204820d19dace42063620dba6786e4293bfabd2166/wandb-0.19.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:ec14280a833263ae828d181b853be38858f933f55ecb77a9040372bf2b09b5e3", size = 20015933 }, + { url = "https://files.pythonhosted.org/packages/59/6e/3bf8590ccbce0eb7c705c0f2b1e3700fad891d48524a93e86f52f7000f67/wandb-0.19.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3d2275ef9d97ce8203b56621d710276b2c023ab3f1a9837dccaf5d75b819ab38", size = 19241348 }, + { url = "https://files.pythonhosted.org/packages/15/93/6b6f4b163a20c70ce4f2792b6ac15c2322dc0cbd1b7377b5aca8c43526df/wandb-0.19.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:65c4fc6fd537d554bcab31a74f28bba82782f83f735b6972702dbab31caaecf1", size = 20040571 }, + { url = "https://files.pythonhosted.org/packages/50/db/efdb733bb98038434ce097ae8178039e81aa4bbc182b3ebc3bb244b29a3c/wandb-0.19.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f0fec8825702ec4ac8453652f2af69b211ee73895272bbdb625bb2721da1f4", size = 18821680 }, + { url = "https://files.pythonhosted.org/packages/45/e0/7ba3b78a74413b7467300cb7a5d486b9871ee464a7cade98ea869d3ca3df/wandb-0.19.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146b972a0d11442f6b5592e5b53ae37b5add5131206136e5bf0a8c3e3fb8fbd0", size = 20095363 }, + { url = "https://files.pythonhosted.org/packages/0d/9a/828968f7c6256a2440123f9602a403fe2f7730a3286e0e344a84cb9a0821/wandb-0.19.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:370d96c23217cd5a16c1f56e02cda9b0f1e2805f4dd6fa942645a726a0e9b549", size = 20169998 }, + { url = "https://files.pythonhosted.org/packages/43/5b/3f436aa647681bf1b6a3fd694c974a40d33be5c40b492277020da360a5cf/wandb-0.19.0-py3-none-win32.whl", hash = "sha256:ab50cc3233727765fbb7b9266cf824f53637c8de2be47ba107542e3ad21ba307", size = 19563293 }, + { url = "https://files.pythonhosted.org/packages/cd/c8/c778a55aeab47ea918c82023fad837cdf5e686dc2249b67a723d452da390/wandb-0.19.0-py3-none-win_amd64.whl", hash = "sha256:0fe8af679306b959b22260b4a67f22186829433809f76e48e70d25c04c2dcf94", size = 19563296 }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "websockets" +version = "14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, + { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, + { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, + { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, + { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, + { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, + { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, + { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, + { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, + { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, + { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, + { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, + { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, + { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, + { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, + { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, + { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, + { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, + { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, + { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, + { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, + { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, + { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, + { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, + { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, + { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, + { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, + { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, +] + +[[package]] +name = "wrapt" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/a1/fc03dca9b0432725c2e8cdbf91a349d2194cf03d8523c124faebe581de09/wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", size = 55542 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/40/def56538acddc2f764c157d565b9f989072a1d2f2a8e384324e2e104fc7d/wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a", size = 38766 }, + { url = "https://files.pythonhosted.org/packages/89/e2/8c299f384ae4364193724e2adad99f9504599d02a73ec9199bf3f406549d/wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed", size = 83730 }, + { url = "https://files.pythonhosted.org/packages/29/ef/fcdb776b12df5ea7180d065b28fa6bb27ac785dddcd7202a0b6962bbdb47/wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489", size = 75470 }, + { url = "https://files.pythonhosted.org/packages/55/b5/698bd0bf9fbb3ddb3a2feefbb7ad0dea1205f5d7d05b9cbab54f5db731aa/wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9", size = 83168 }, + { url = "https://files.pythonhosted.org/packages/ce/07/701a5cee28cb4d5df030d4b2649319e36f3d9fdd8000ef1d84eb06b9860d/wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/42/92/c48ba92cda6f74cb914dc3c5bba9650dc80b790e121c4b987f3a46b028f5/wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d", size = 75101 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/9276d3269334138b88a2947efaaf6335f61d547698e50dff672ade24f2c6/wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b", size = 81835 }, + { url = "https://files.pythonhosted.org/packages/b9/4c/39595e692753ef656ea94b51382cc9aea662fef59d7910128f5906486f0e/wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346", size = 36412 }, + { url = "https://files.pythonhosted.org/packages/63/bb/c293a67fb765a2ada48f48cd0f2bb957da8161439da4c03ea123b9894c02/wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a", size = 38744 }, + { url = "https://files.pythonhosted.org/packages/85/82/518605474beafff11f1a34759f6410ab429abff9f7881858a447e0d20712/wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/80/6c/17c3b2fed28edfd96d8417c865ef0b4c955dc52c4e375d86f459f14340f1/wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", size = 88622 }, + { url = "https://files.pythonhosted.org/packages/4a/11/60ecdf3b0fd3dca18978d89acb5d095a05f23299216e925fcd2717c81d93/wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", size = 80920 }, + { url = "https://files.pythonhosted.org/packages/d2/50/dbef1a651578a3520d4534c1e434989e3620380c1ad97e309576b47f0ada/wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", size = 89170 }, + { url = "https://files.pythonhosted.org/packages/44/a2/78c5956bf39955288c9e0dd62e807b308c3aa15a0f611fbff52aa8d6b5ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", size = 86748 }, + { url = "https://files.pythonhosted.org/packages/99/49/2ee413c78fc0bdfebe5bee590bf3becdc1fab0096a7a9c3b5c9666b2415f/wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", size = 79734 }, + { url = "https://files.pythonhosted.org/packages/c0/8c/4221b7b270e36be90f0930fe15a4755a6ea24093f90b510166e9ed7861ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", size = 87552 }, + { url = "https://files.pythonhosted.org/packages/4c/6b/1aaccf3efe58eb95e10ce8e77c8909b7a6b0da93449a92c4e6d6d10b3a3d/wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", size = 36647 }, + { url = "https://files.pythonhosted.org/packages/b3/4f/243f88ac49df005b9129194c6511b3642818b3e6271ddea47a15e2ee4934/wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", size = 38830 }, + { url = "https://files.pythonhosted.org/packages/67/9c/38294e1bb92b055222d1b8b6591604ca4468b77b1250f59c15256437644f/wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/78/b6/76597fb362cbf8913a481d41b14b049a8813cd402a5d2f84e57957c813ae/wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", size = 88608 }, + { url = "https://files.pythonhosted.org/packages/bc/69/b500884e45b3881926b5f69188dc542fb5880019d15c8a0df1ab1dfda1f7/wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", size = 80879 }, + { url = "https://files.pythonhosted.org/packages/52/31/f4cc58afe29eab8a50ac5969963010c8b60987e719c478a5024bce39bc42/wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", size = 89119 }, + { url = "https://files.pythonhosted.org/packages/aa/9c/05ab6bf75dbae7a9d34975fb6ee577e086c1c26cde3b6cf6051726d33c7c/wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", size = 86778 }, + { url = "https://files.pythonhosted.org/packages/0e/6c/4b8d42e3db355603d35fe5c9db79c28f2472a6fd1ccf4dc25ae46739672a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", size = 79793 }, + { url = "https://files.pythonhosted.org/packages/69/23/90e3a2ee210c0843b2c2a49b3b97ffcf9cad1387cb18cbeef9218631ed5a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", size = 87606 }, + { url = "https://files.pythonhosted.org/packages/5f/06/3683126491ca787d8d71d8d340e775d40767c5efedb35039d987203393b7/wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", size = 36651 }, + { url = "https://files.pythonhosted.org/packages/f1/bc/3bf6d2ca0d2c030d324ef9272bea0a8fdaff68f3d1fa7be7a61da88e51f7/wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838", size = 38835 }, + { url = "https://files.pythonhosted.org/packages/ce/b5/251165c232d87197a81cd362eeb5104d661a2dd3aa1f0b33e4bf61dda8b8/wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", size = 40146 }, + { url = "https://files.pythonhosted.org/packages/89/33/1e1bdd3e866eeb73d8c4755db1ceb8a80d5bd51ee4648b3f2247adec4e67/wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", size = 113444 }, + { url = "https://files.pythonhosted.org/packages/9f/7c/94f53b065a43f5dc1fbdd8b80fd8f41284315b543805c956619c0b8d92f0/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", size = 101246 }, + { url = "https://files.pythonhosted.org/packages/62/5d/640360baac6ea6018ed5e34e6e80e33cfbae2aefde24f117587cd5efd4b7/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", size = 109320 }, + { url = "https://files.pythonhosted.org/packages/e3/cf/6c7a00ae86a2e9482c91170aefe93f4ccda06c1ac86c4de637c69133da59/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", size = 110193 }, + { url = "https://files.pythonhosted.org/packages/cd/cc/aa718df0d20287e8f953ce0e2f70c0af0fba1d3c367db7ee8bdc46ea7003/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", size = 100460 }, + { url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 }, + { url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 }, + { url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 }, + { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 }, +] + +[[package]] +name = "xxhash" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/c7/afed0f131fbda960ff15eee7f304fa0eeb2d58770fade99897984852ef23/xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/8c/0c/7c3bc6d87e5235672fcc2fb42fd5ad79fe1033925f71bf549ee068c7d1ca/xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8", size = 30800 }, + { url = "https://files.pythonhosted.org/packages/04/9e/01067981d98069eec1c20201f8c145367698e9056f8bc295346e4ea32dd1/xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166", size = 221566 }, + { url = "https://files.pythonhosted.org/packages/d4/09/d4996de4059c3ce5342b6e1e6a77c9d6c91acce31f6ed979891872dd162b/xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7", size = 201214 }, + { url = "https://files.pythonhosted.org/packages/62/f5/6d2dc9f8d55a7ce0f5e7bfef916e67536f01b85d32a9fbf137d4cadbee38/xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623", size = 429433 }, + { url = "https://files.pythonhosted.org/packages/d9/72/9256303f10e41ab004799a4aa74b80b3c5977d6383ae4550548b24bd1971/xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a", size = 194822 }, + { url = "https://files.pythonhosted.org/packages/34/92/1a3a29acd08248a34b0e6a94f4e0ed9b8379a4ff471f1668e4dce7bdbaa8/xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88", size = 208538 }, + { url = "https://files.pythonhosted.org/packages/53/ad/7fa1a109663366de42f724a1cdb8e796a260dbac45047bce153bc1e18abf/xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c", size = 216953 }, + { url = "https://files.pythonhosted.org/packages/35/02/137300e24203bf2b2a49b48ce898ecce6fd01789c0fcd9c686c0a002d129/xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2", size = 203594 }, + { url = "https://files.pythonhosted.org/packages/23/03/aeceb273933d7eee248c4322b98b8e971f06cc3880e5f7602c94e5578af5/xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084", size = 210971 }, + { url = "https://files.pythonhosted.org/packages/e3/64/ed82ec09489474cbb35c716b189ddc1521d8b3de12b1b5ab41ce7f70253c/xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d", size = 415050 }, + { url = "https://files.pythonhosted.org/packages/71/43/6db4c02dcb488ad4e03bc86d70506c3d40a384ee73c9b5c93338eb1f3c23/xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839", size = 192216 }, + { url = "https://files.pythonhosted.org/packages/22/6d/db4abec29e7a567455344433d095fdb39c97db6955bb4a2c432e486b4d28/xxhash-3.5.0-cp311-cp311-win32.whl", hash = "sha256:109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da", size = 30120 }, + { url = "https://files.pythonhosted.org/packages/52/1c/fa3b61c0cf03e1da4767213672efe186b1dfa4fc901a4a694fb184a513d1/xxhash-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58", size = 30003 }, + { url = "https://files.pythonhosted.org/packages/6b/8e/9e6fc572acf6e1cc7ccb01973c213f895cb8668a9d4c2b58a99350da14b7/xxhash-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3", size = 26777 }, + { url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787 }, + { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959 }, + { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006 }, + { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326 }, + { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380 }, + { url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934 }, + { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301 }, + { url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351 }, + { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294 }, + { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674 }, + { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022 }, + { url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170 }, + { url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040 }, + { url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796 }, + { url = "https://files.pythonhosted.org/packages/c9/b8/e4b3ad92d249be5c83fa72916c9091b0965cb0faeff05d9a0a3870ae6bff/xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6", size = 31795 }, + { url = "https://files.pythonhosted.org/packages/fc/d8/b3627a0aebfbfa4c12a41e22af3742cf08c8ea84f5cc3367b5de2d039cce/xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5", size = 30792 }, + { url = "https://files.pythonhosted.org/packages/c3/cc/762312960691da989c7cd0545cb120ba2a4148741c6ba458aa723c00a3f8/xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc", size = 220950 }, + { url = "https://files.pythonhosted.org/packages/fe/e9/cc266f1042c3c13750e86a535496b58beb12bf8c50a915c336136f6168dc/xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3", size = 199980 }, + { url = "https://files.pythonhosted.org/packages/bf/85/a836cd0dc5cc20376de26b346858d0ac9656f8f730998ca4324921a010b9/xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c", size = 428324 }, + { url = "https://files.pythonhosted.org/packages/b4/0e/15c243775342ce840b9ba34aceace06a1148fa1630cd8ca269e3223987f5/xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb", size = 194370 }, + { url = "https://files.pythonhosted.org/packages/87/a1/b028bb02636dfdc190da01951d0703b3d904301ed0ef6094d948983bef0e/xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f", size = 207911 }, + { url = "https://files.pythonhosted.org/packages/80/d5/73c73b03fc0ac73dacf069fdf6036c9abad82de0a47549e9912c955ab449/xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7", size = 216352 }, + { url = "https://files.pythonhosted.org/packages/b6/2a/5043dba5ddbe35b4fe6ea0a111280ad9c3d4ba477dd0f2d1fe1129bda9d0/xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326", size = 203410 }, + { url = "https://files.pythonhosted.org/packages/a2/b2/9a8ded888b7b190aed75b484eb5c853ddd48aa2896e7b59bbfbce442f0a1/xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf", size = 210322 }, + { url = "https://files.pythonhosted.org/packages/98/62/440083fafbc917bf3e4b67c2ade621920dd905517e85631c10aac955c1d2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7", size = 414725 }, + { url = "https://files.pythonhosted.org/packages/75/db/009206f7076ad60a517e016bb0058381d96a007ce3f79fa91d3010f49cc2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c", size = 192070 }, + { url = "https://files.pythonhosted.org/packages/1f/6d/c61e0668943a034abc3a569cdc5aeae37d686d9da7e39cf2ed621d533e36/xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637", size = 30172 }, + { url = "https://files.pythonhosted.org/packages/96/14/8416dce965f35e3d24722cdf79361ae154fa23e2ab730e5323aa98d7919e/xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43", size = 30041 }, + { url = "https://files.pythonhosted.org/packages/27/ee/518b72faa2073f5aa8e3262408d284892cb79cf2754ba0c3a5870645ef73/xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b", size = 26801 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, + { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, + { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, + { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, + { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, + { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, + { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, + { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, + { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, + { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, + { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, + { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, + { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, + { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +]