diff --git a/.github/workflows/container-maintenance.yml b/.github/workflows/container-maintenance.yml index ff75a3f4..86d54263 100644 --- a/.github/workflows/container-maintenance.yml +++ b/.github/workflows/container-maintenance.yml @@ -9,6 +9,9 @@ on: concurrency: group: ${{ github.workflow }} +permissions: + packages: write + jobs: cleanup-container-tags: runs-on: ubuntu-latest @@ -16,12 +19,12 @@ jobs: - name: Delete PR and untagged images older than 2 weeks uses: snok/container-retention-policy@v3.0.0 with: - account: ${{ github.actor }} + account: ${{ github.repository_owner }} token: ${{ github.token }} image-names: ${{ github.event.repository.name }} image-tags: "pr-*" cut-off: 2w - dry-run: true + dry-run: false push-container-tags: runs-on: ubuntu-latest @@ -31,19 +34,20 @@ jobs: - name: Log into ghcr.io uses: redhat-actions/podman-login@v1 with: - username: ${{ github.actor }} + username: ${{ github.repository_owner }} password: ${{ github.token }} registry: ghcr.io/${{ github.repository_owner }} - name: Get list of tags run: | - skopeo list-tags docker://${{ github.repository }} | jq --raw-output '.Tags[]' > tags + set -euo pipefail # Fail pipe if any command fails + skopeo list-tags docker://ghcr.io/${{ github.repository }} | jq --raw-output '.Tags[]' > tags - name: Get latest release and rc tags run: | STABLE_TAG="$(grep -P '^v\d+\.\d+\.\d+$' tags | sort -rV | head -n1)" - echo "STABLE_TAG=${STABLE_TAG:-v0.0.0}" >> $GITHUB_ENV + echo "stable_tag=${STABLE_TAG:-v0.0.0}" >> $GITHUB_ENV LATEST_TAG="$(grep -P '^v\d+\.\d+\.\d+' tags | sort -rV | head -n1)" - echo "LATEST_TAG=${LATEST_TAG:-v0.0.0}" >> $GITHUB_ENV + echo "latest_tag=${LATEST_TAG:-v0.0.0}" >> $GITHUB_ENV - name: Update latest and stable tags run: | - skopeo copy docker://${{ github.repository }}:${{ env.stable_tag }} docker://${{ github.repository }}:stable - skopeo copy docker://${{ github.repository }}:${{ env.latest_tag }} docker://${{ github.repository }}:latest + skopeo copy docker://ghcr.io/${{ github.repository }}:${{ env.stable_tag }} docker://ghcr.io/${{ github.repository }}:stable + skopeo copy docker://ghcr.io/${{ github.repository }}:${{ env.latest_tag }} docker://ghcr.io/${{ github.repository }}:latest diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 903e12ba..276913e0 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -228,7 +228,7 @@ jobs: uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./ui/out + publish_dir: .src/ui/out destination_dir: ui/release/${TAG} keep_files: false user_name: ${{ github.actor }} @@ -298,7 +298,12 @@ jobs: with: fetch-depth: 0 - name: Get version from branch - run: echo "PACKAGE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + run: | + echo "package_version=${GITHUB_REF#refs/heads/release/}" >> $GITHUB_ENV + - name: Fail if version is unset + if: ${{ env.package_version == '' }} + run: | + exit 1 - name: Buildah build id: build-image uses: redhat-actions/buildah-build@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44b40250..e7e2499b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -227,7 +227,7 @@ jobs: uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./ui/out + publish_dir: ./src/ui/out destination_dir: ui/${TAG} keep_files: false user_name: ${{ github.actor }} @@ -297,7 +297,12 @@ jobs: with: fetch-depth: 0 - name: Get version from branch - run: echo "PACKAGE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + run: | + echo "package_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + - name: Fail if version is unset + if: ${{ env.package_version == '' }} + run: | + exit 1 - name: Buildah build id: build-image uses: redhat-actions/buildah-build@v2 diff --git a/Containerfile b/Containerfile index 1f935623..d88b33f2 100644 --- a/Containerfile +++ b/Containerfile @@ -31,19 +31,28 @@ COPY / /src # Install guidellm and locked dependencies RUN pdm use -p /src -f /opt/app-root \ - && pdm install -p /src --check --prod --no-editable + && pdm install -p /src -G all --check --prod --no-editable # Prod image FROM $BASE_IMAGE +# Switch to root for installing packages +USER root + +# Install some helpful utilities and deps +RUN dnf install -y --setopt=install_weak_deps=False \ + vi tar rsync ffmpeg-free \ + && dnf clean all + +# Switch back to unpriv user +# Root group for k8s +USER 1001:0 + # Add guidellm bin to PATH # Argument defaults can be set with GUIDELLM_ ENV HOME="/home/guidellm" \ GUIDELLM_OUTPUT_PATH="/results/benchmarks.json" -# Make sure root is the primary group -USER 1001:0 - # Create the user home dir WORKDIR $HOME diff --git a/docs/assets/sample-output1.png b/docs/assets/sample-output1.png new file mode 100644 index 00000000..526d3475 Binary files /dev/null and b/docs/assets/sample-output1.png differ diff --git a/docs/assets/sample-output2.png b/docs/assets/sample-output2.png new file mode 100644 index 00000000..98ffad8e Binary files /dev/null and b/docs/assets/sample-output2.png differ diff --git a/docs/assets/sample-output3.png b/docs/assets/sample-output3.png new file mode 100644 index 00000000..c5d763a4 Binary files /dev/null and b/docs/assets/sample-output3.png differ diff --git a/docs/examples/practice_on_vllm_simulator.md b/docs/examples/practice_on_vllm_simulator.md new file mode 100644 index 00000000..80e85774 --- /dev/null +++ b/docs/examples/practice_on_vllm_simulator.md @@ -0,0 +1,117 @@ +# GuideLLM Benchmark Testing Best Practice + +Do first easy-go guidellm benchmark testing from scratch using vLLM Simulator. + +## Getting Started + +### 📦 1. Benchmark Testing Environment Setup + +#### 1.1 Create a Conda Environment (recommended) + +```bash +conda create -n guidellm-bench python=3.11 -y +conda activate guidellm-bench +``` + +#### 1.2 Install Dependencies + +```bash +git clone https://github.com/vllm-project/guidellm.git +cd guidellm +pip install guidellm +``` + +For more detailed instructions, refer to [GuideLLM README](https://github.com/vllm-project/guidellm/blob/main/README.md). + +#### 1.3 Verify Installation + +```bash +guidellm --help +``` + +#### 1.4 Startup OpenAI-compatible API in vLLM simulator docker container + +```bash +docker pull ghcr.io/llm-d/llm-d-inference-sim:v0.4.0 + +docker run --rm --publish 8000:8000 \ +ghcr.io/llm-d/llm-d-inference-sim:v0.4.0 \ +--port 8000 \ +--model "Qwen/Qwen2.5-1.5B-Instruct" \ +--lora-modules '{"name":"tweet-summary-0"}' '{"name":"tweet-summary-1"}' +``` + +For more detailed instructions, refer to: [vLLM Simulator](https://llm-d.ai/docs/architecture/Components/inference-sim) + +Docker image versions: [Docker Images](https://github.com/llm-d/llm-d-inference-sim/pkgs/container/llm-d-inference-sim) + +Check open-ai api working via curl: + +- check /v1/models + +```bash +curl --request GET 'http://localhost:8000/v1/models' +``` + +- check /v1/chat/completions + +```bash +curl --request POST 'http://localhost:8000/v1/chat/completions' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "model": "tweet-summary-0", + "stream": false, + "messages": [{"role": "user", "content": "Say this is a test!"}] +}' +``` + +- check /v1/completions + +```bash +curl --request POST 'http://localhost:8000/v1/completions' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "model": "tweet-summary-0", + "stream": false, + "prompt": "Say this is a test!", + "max_tokens": 128 +}' +``` + +#### 1.5 Download Tokenizer + +Download Qwen/Qwen2.5-1.5B-Instruct tokenizer files from [Qwen/Qwen2.5-1.5B-Instruct](https://modelscope.cn/models/Qwen/Qwen2.5-1.5B-Instruct/files) save to local path such as ${local_path}/Qwen2.5-1.5B-Instruct + +```bash +ls ./Qwen2.5-1.5B-Instruct +merges.txt tokenizer.json tokenizer_config.json vocab.json +``` + +______________________________________________________________________ + +## 🚀 2. Running Benchmarks + +```bash +guidellm benchmark \ +--target "http://localhost:8000/" \ +--model "tweet-summary-0" \ +--processor "${local_path}/Qwen2.5-1.5B-Instruct" \ +--rate-type sweep \ +--max-seconds 10 \ +--max-requests 10 \ +--data "prompt_tokens=128,output_tokens=56" +``` + +______________________________________________________________________ + +## 📊 3. Results Interpretation + +![alt text](../assets/sample-output1.png) ![alt text](../assets/sample-output2.png) ![alt text](../assets/sample-output3.png) + +After the benchmark completes, key results are clear and straightforward, such as: + +- **`TTFT`**: Time to First Token +- **`TPOT`**: Time Per Output Token +- **`ITL`**: Inter-Token Latency + +The first benchmark test complete. diff --git a/pylock.toml b/pylock.toml index 11fecb48..c4a1545d 100644 --- a/pylock.toml +++ b/pylock.toml @@ -6,11 +6,151 @@ environments = [ "python_version ~= \"3.12\"", "python_full_version >= \"3.10.0\" and python_version < \"3.12\"", ] -extras = ["dev", "recommended"] +extras = ["all", "audio", "dev", "openai", "perf", "recommended", "vision"] dependency-groups = ["default"] default-groups = ["default"] created-by = "pdm" +[[packages]] +name = "torch" +version = "2.9.0+cpu" +requires-python = ">=3.10" +wheels = [ + {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "44aadb735774d4a99525d2ec29126b23016c44a07b02ce6c237dfa61a223dd52"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "b355e07b7f0c369cb031adfcbff5c37a609abcea091b918a39886412afd2e07d"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-win_amd64.whl",hashes = {sha256 = "c2698999361d73c2d25d7cc8a787130188d49b183abb18b554228daa102e1594"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fa0d1373d04b30ff8f12d542135d292f1a1ddb7c0d852a3d487a320360e5dab9"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2f49bb57a5fe0dc7f8e73ea9e5d36ebda2ea25b8a714a788f0fc2fc47d20a830"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "3a60d1ecf27a9cce839b3aa665b26f0af1b1007b9c9f1e7f597f6b7bdf107617"}}, + {name = "torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "d8e2ab7f86010330bdcc39c8b2c795590cc75e37df4823cdaee2c98d6e3ff4a3"}}, + {name = "torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "a3e859039c985d8e3ea60d7a54ca7e97ea2ae15e31beced4f3260128a161bb01"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "be4438d8dad7f0d5a5e54f0feef8a893446894ec87f102bb1d82dcc4518542e4"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6c9b217584400963d5b4daddb3711ec7a3778eab211e18654fba076cce3b8682"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "728372e3f58c5826445f677746e5311c1935c1a7c59599f73a49ded850e038e8"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "95e56c26f919fbb98f16e7a0b87af494b893f9da9a65a020f17a01c13e520a81"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "6c777160288b08555820781ae0f3a2c67a59bd24b065e88ca1ec20e2f9dc8ac7"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "528fd338311f31c9fb18038cafd00e6eae0bf5ad5577521701acb62510753d18"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "d572863990e7d2762b547735ef589f6350d9eb4e441d38753a1c33636698cf4c"}}, + {name = "torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "259548471194ab63d7ea273873053a6e3cc23530c1510f01e9d7ad259187bbd0"}}, + {name = "torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "e24836d968b54ef4dfb05594001a61958711ac9224026291e4e3f92f83a6fd7f"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "3a651434ae1248b0568c12b5f9e3acc8942eb28378d9d04a79302938b68c6f24"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "28f6eb31b08180a5c5e98d5bc14eef6909c9f5a1dbff9632c3e02a8773449349"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "e438061b87ec7dd6018fca9f975219889aa0a3f6cdc3ea10dd0ae2bc7f1c47ce"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "eb13ff1c34e338d722e76a4fd83b8d282782505bd1b99af4b3c32da66eba6eb4"}}, + {name = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "4de0ed8cbc457a506dbca40376e206a29efee10756a00f1f3404bf67ad737d04"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "filelock", + "typing-extensions>=4.10.0", + "setuptools; python_version >= \"3.12\"", + "sympy>=1.13.3", + "networkx>=2.5.1", + "jinja2", + "fsspec>=0.8.5", +] + +[[packages]] +name = "torchcodec" +version = "0.8.0" +requires-python = ">=3.8" +wheels = [ + {name = "torchcodec-0.8.0-cp313-cp313-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/9b/1c/40fd9358e5dd958775b8d0a01c962a022884810f441ac28229ed0e811599/torchcodec-0.8.0-cp313-cp313-macosx_12_0_arm64.whl",hashes = {sha256 = "1f3309252d035c888e6ae4518f5aca24f1c38f163124792d8a29a6872bf457f2"}}, + {name = "torchcodec-0.8.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/27/81/2e8f8657aed983f20f9ce842b19016d4aff05dd608ac0def94e013602814/torchcodec-0.8.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "253cc3c7a17c7be26abfcf2470e8eab3803ff3108f70be060a7efdcb49d917bc"}}, + {name = "torchcodec-0.8.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/09/1f/b09f028822991241eb1a31931749d034aee2c654d00f1930f4cecce595bc/torchcodec-0.8.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "c69285cb393c3b36c7bcc4e59e304076ea22b350ff6adca4a2a09b5f3f81f15c"}}, + {name = "torchcodec-0.8.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/17/ae/8b1d69e653894243fa66e2fec511cf203107dd146d161c9f095893c13bbc/torchcodec-0.8.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "af82d1fac3667335e089dc958b5e8eef5458e37d65cb3a94ebf81f45f00f7805"}}, + {name = "torchcodec-0.8.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f6/fd/eec92c82545038a90ffd24e3626bb3a85f7d51577b04819c1c753d380a9b/torchcodec-0.8.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2ec2e874dfb6fbf9bbeb792bea56317529636e78db175f56aad1e4efd6e12502"}}, + {name = "torchcodec-0.8.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fe/09/ce7436151a3825f27c00263d722b0cf093609921da6cf24b0fa8133cc415/torchcodec-0.8.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "318da9af9179d156be0a84296e909d51e4cd758598eaaea08c828790c80bf977"}}, +] +marker = "\"all\" in extras or \"audio\" in extras or \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "blobfile" +version = "3.1.0" +requires-python = ">=3.8.0" +sdist = {name = "blobfile-3.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/6d/2e7567da75ddbb24fe979f52284b708da349d67a41042635af36071a5a6b/blobfile-3.1.0.tar.gz", hashes = {sha256 = "d45b6b1fa3b0920732314c23ddbdb4f494ca12f787c2b6eb6bba6faa51382671"}} +wheels = [ + {name = "blobfile-3.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/a7/51af11120d75af2828f8eede0b13a4caff650d708ac50e62d000aefe1ffb/blobfile-3.1.0-py3-none-any.whl",hashes = {sha256 = "2b4c5e766ebb7dfa20e4990cf6ec3d2106bdc91d632fb9377f170a234c5a5c6a"}}, +] +marker = "\"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" + +[packages.tool.pdm] +dependencies = [ + "pycryptodomex>=3.8", + "urllib3<3,>=1.25.3", + "lxml>=4.9", + "filelock>=3.0", +] + +[[packages]] +name = "tiktoken" +version = "0.12.0" +requires-python = ">=3.9" +sdist = {name = "tiktoken-0.12.0.tar.gz", url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hashes = {sha256 = "b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931"}} +wheels = [ + {name = "tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646"}}, + {name = "tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88"}}, + {name = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff"}}, + {name = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830"}}, + {name = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b"}}, + {name = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b"}}, + {name = "tiktoken-0.12.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71"}}, + {name = "tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3"}}, + {name = "tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160"}}, + {name = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa"}}, + {name = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be"}}, + {name = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a"}}, + {name = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3"}}, + {name = "tiktoken-0.12.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f"}}, + {name = "tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8"}}, + {name = "tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b"}}, + {name = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37"}}, + {name = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad"}}, + {name = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5"}}, + {name = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3"}}, + {name = "tiktoken-0.12.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd"}}, + {name = "tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb"}}, + {name = "tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa"}}, + {name = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc"}}, + {name = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded"}}, + {name = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd"}}, + {name = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967"}}, + {name = "tiktoken-0.12.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def"}}, + {name = "tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970"}}, + {name = "tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16"}}, + {name = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030"}}, + {name = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134"}}, + {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a"}}, + {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892"}}, + {name = "tiktoken-0.12.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1"}}, +] +marker = "\"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" + +[packages.tool.pdm] +dependencies = [ + "regex>=2022.1.18", + "requests>=2.26.0", +] + [[packages]] name = "click" version = "8.1.8" @@ -44,24 +184,6 @@ dependencies = [ "tomli>=2.0.1; python_version < \"3.11\"", ] -[[packages]] -name = "blobfile" -version = "3.1.0" -requires-python = ">=3.8.0" -sdist = {name = "blobfile-3.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/6d/2e7567da75ddbb24fe979f52284b708da349d67a41042635af36071a5a6b/blobfile-3.1.0.tar.gz", hashes = {sha256 = "d45b6b1fa3b0920732314c23ddbdb4f494ca12f787c2b6eb6bba6faa51382671"}} -wheels = [ - {name = "blobfile-3.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/a7/51af11120d75af2828f8eede0b13a4caff650d708ac50e62d000aefe1ffb/blobfile-3.1.0-py3-none-any.whl",hashes = {sha256 = "2b4c5e766ebb7dfa20e4990cf6ec3d2106bdc91d632fb9377f170a234c5a5c6a"}}, -] -marker = "\"recommended\" in extras" - -[packages.tool.pdm] -dependencies = [ - "pycryptodomex>=3.8", - "urllib3<3,>=1.25.3", - "lxml>=4.9", - "filelock>=3.0", -] - [[packages]] name = "build" version = "1.3.0" @@ -96,6 +218,169 @@ dependencies = [ "aiologic>=0.13.0", ] +[[packages]] +name = "datasets" +version = "4.2.0" +requires-python = ">=3.9.0" +sdist = {name = "datasets-4.2.0.tar.gz", url = "https://files.pythonhosted.org/packages/70/48/0186fbc4b86a4f9ecaf04eb01e877e78b53bfa0b03be9c84b2298431ba33/datasets-4.2.0.tar.gz", hashes = {sha256 = "8333a7db9f3bb8044c1b819a35d4e3e2809596c837793b0921382efffdc36e78"}} +wheels = [ + {name = "datasets-4.2.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/91/9e/0bbbd09b116fd8ee2d3617e28e6598551d2f0f24d3a2ce99cc87ec85aeb0/datasets-4.2.0-py3-none-any.whl",hashes = {sha256 = "fdc43aaf4a73b31f64f80f72f195ab413a1141ed15555d675b2fd17926f8b026"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" + +[packages.tool.pdm] +dependencies = [ + "filelock", + "numpy>=1.17", + "pyarrow>=21.0.0", + "dill<0.4.1,>=0.3.0", + "pandas", + "requests>=2.32.2", + "httpx<1.0.0", + "tqdm>=4.66.3", + "xxhash", + "multiprocess<0.70.17", + "fsspec[http]<=2025.9.0,>=2023.1.0", + "huggingface-hub<2.0,>=0.25.0", + "packaging", + "pyyaml>=5.1", +] + +[[packages]] +name = "numpy" +version = "2.3.3" +requires-python = ">=3.11" +sdist = {name = "numpy-2.3.3.tar.gz", url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hashes = {sha256 = "ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}} +wheels = [ + {name = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",hashes = {sha256 = "50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",hashes = {sha256 = "b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}}, + {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}}, + {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}}, + {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}}, + {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}}, + {name = "numpy-2.3.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl",hashes = {sha256 = "cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}}, + {name = "numpy-2.3.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}}, + {name = "numpy-2.3.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",hashes = {sha256 = "7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",hashes = {sha256 = "533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}}, + {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}}, + {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}}, + {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}}, + {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}}, + {name = "numpy-2.3.3-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl",hashes = {sha256 = "1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}}, + {name = "numpy-2.3.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}}, + {name = "numpy-2.3.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}}, + {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}}, + {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}}, + {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}}, + {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}}, + {name = "numpy-2.3.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl",hashes = {sha256 = "9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}}, + {name = "numpy-2.3.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}}, + {name = "numpy-2.3.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}}, + {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}}, + {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}}, + {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}}, + {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}}, + {name = "numpy-2.3.3-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl",hashes = {sha256 = "a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}}, + {name = "numpy-2.3.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}}, + {name = "numpy-2.3.3-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}}, + {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}}, + {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}}, + {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}}, + {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}}, + {name = "numpy-2.3.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl",hashes = {sha256 = "5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}}, + {name = "numpy-2.3.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}}, + {name = "numpy-2.3.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}}, +] +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"audio\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"vision\" in extras and python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "pyyaml" +version = "6.0.3" +requires-python = ">=3.8" +sdist = {name = "pyyaml-6.0.3.tar.gz", url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hashes = {sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}} +wheels = [ + {name = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}}, + {name = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}}, + {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}}, + {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}}, + {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}}, + {name = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}}, + {name = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}}, + {name = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}}, + {name = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}}, + {name = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}}, + {name = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}}, + {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}}, + {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}}, + {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}}, + {name = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}}, + {name = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}}, + {name = "pyyaml-6.0.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl",hashes = {sha256 = "d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}}, + {name = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}}, + {name = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}}, + {name = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}}, + {name = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}}, + {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}}, + {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}}, + {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}}, + {name = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}}, + {name = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}}, + {name = "pyyaml-6.0.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl",hashes = {sha256 = "96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}}, + {name = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}}, + {name = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}}, + {name = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}}, + {name = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}}, + {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}}, + {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}}, + {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}}, + {name = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}}, + {name = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}}, + {name = "pyyaml-6.0.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl",hashes = {sha256 = "8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}}, + {name = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}}, + {name = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}}, + {name = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}}, + {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}}, + {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}}, + {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}}, + {name = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}}, + {name = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}}, + {name = "pyyaml-6.0.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl",hashes = {sha256 = "28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}}, + {name = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "ftfy" version = "6.3.1" @@ -266,88 +551,20 @@ dependencies = [ "virtualenv>=20.10.0", ] -[[packages]] -name = "pyyaml" -version = "6.0.3" -requires-python = ">=3.8" -sdist = {name = "pyyaml-6.0.3.tar.gz", url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hashes = {sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}} -wheels = [ - {name = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}}, - {name = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}}, - {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}}, - {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}}, - {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}}, - {name = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}}, - {name = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}}, - {name = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}}, - {name = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}}, - {name = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}}, - {name = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}}, - {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}}, - {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}}, - {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}}, - {name = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}}, - {name = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}}, - {name = "pyyaml-6.0.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl",hashes = {sha256 = "d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}}, - {name = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}}, - {name = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}}, - {name = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}}, - {name = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}}, - {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}}, - {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}}, - {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}}, - {name = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}}, - {name = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}}, - {name = "pyyaml-6.0.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl",hashes = {sha256 = "96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}}, - {name = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}}, - {name = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}}, - {name = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}}, - {name = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}}, - {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}}, - {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}}, - {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}}, - {name = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}}, - {name = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}}, - {name = "pyyaml-6.0.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl",hashes = {sha256 = "8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}}, - {name = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}}, - {name = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}}, - {name = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}}, - {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}}, - {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}}, - {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}}, - {name = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}}, - {name = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}}, - {name = "pyyaml-6.0.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl",hashes = {sha256 = "28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}}, - {name = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}}, -] -marker = "\"default\" in dependency_groups or \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "pydantic" -version = "2.12.0" +version = "2.12.2" requires-python = ">=3.9" -sdist = {name = "pydantic-2.12.0.tar.gz", url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hashes = {sha256 = "c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563"}} +sdist = {name = "pydantic-2.12.2.tar.gz", url = "https://files.pythonhosted.org/packages/8d/35/d319ed522433215526689bad428a94058b6dd12190ce7ddd78618ac14b28/pydantic-2.12.2.tar.gz", hashes = {sha256 = "7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd"}} wheels = [ - {name = "pydantic-2.12.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl",hashes = {sha256 = "f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f"}}, + {name = "pydantic-2.12.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6c/98/468cb649f208a6f1279448e6e5247b37ae79cf5e4041186f1e2ef3d16345/pydantic-2.12.2-py3-none-any.whl",hashes = {sha256 = "25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae"}}, ] marker = "\"default\" in dependency_groups" [packages.tool.pdm] dependencies = [ "annotated-types>=0.6.0", - "pydantic-core==2.41.1", + "pydantic-core==2.41.4", "typing-extensions>=4.14.1", "typing-inspection>=0.4.2", ] @@ -525,108 +742,41 @@ wheels = [ {name = "scipy-1.16.2-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779"}}, {name = "scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl",url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl",hashes = {sha256 = "84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70"}}, {name = "scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl",hashes = {sha256 = "5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9"}}, - {name = "scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5"}}, - {name = "scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925"}}, - {name = "scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9"}}, - {name = "scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7"}}, - {name = "scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb"}}, - {name = "scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e"}}, - {name = "scipy-1.16.2-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl",hashes = {sha256 = "fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c"}}, - {name = "scipy-1.16.2-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl",hashes = {sha256 = "2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104"}}, - {name = "scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl",url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl",hashes = {sha256 = "53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1"}}, - {name = "scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl",hashes = {sha256 = "9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a"}}, - {name = "scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f"}}, - {name = "scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4"}}, - {name = "scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21"}}, - {name = "scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7"}}, - {name = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8"}}, - {name = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472"}}, - {name = "scipy-1.16.2-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351"}}, - {name = "scipy-1.16.2-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d"}}, - {name = "scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/8d/6396e00db1282279a4ddd507c5f5e11f606812b608ee58517ce8abbf883f/scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl",hashes = {sha256 = "89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d"}}, - {name = "scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3b/93/ea9edd7e193fceb8eef149804491890bde73fb169c896b61aa3e2d1e4e77/scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl",hashes = {sha256 = "ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371"}}, - {name = "scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/91/4d/281fddc3d80fd738ba86fd3aed9202331180b01e2c78eaae0642f22f7e83/scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0"}}, - {name = "scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/69/40/b33b74c84606fd301b2915f0062e45733c6ff5708d121dd0deaa8871e2d0/scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232"}}, - {name = "scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/55/a7/22c739e2f21a42cc8f16bc76b47cff4ed54fbe0962832c589591c2abec34/scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1"}}, - {name = "scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/53/11/a0160990b82999b45874dc60c0c183d3a3a969a563fffc476d5a9995c407/scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f"}}, - {name = "scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/96/53/7ef48a4cfcf243c3d0f1643f5887c81f29fdf76911c4e49331828e19fc0a/scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef"}}, - {name = "scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/49/7f/71a69e0afd460049d41c65c630c919c537815277dfea214031005f474d78/scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1"}}, - {name = "scipy-1.16.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/95/20e02ca66fb495a95fba0642fd48e0c390d0ece9b9b14c6e931a60a12dea/scipy-1.16.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e"}}, - {name = "scipy-1.16.2-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/92/ad/13646b9beb0a95528ca46d52b7babafbe115017814a611f2065ee4e61d20/scipy-1.16.2-cp312-cp312-win_arm64.whl",hashes = {sha256 = "2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851"}}, -] -marker = "python_version ~= \"3.12\"" - -[packages.tool.pdm] -dependencies = [ - "numpy<2.6,>=1.25.2", -] - -[[packages]] -name = "numpy" -version = "2.3.3" -requires-python = ">=3.11" -sdist = {name = "numpy-2.3.3.tar.gz", url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hashes = {sha256 = "ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}} -wheels = [ - {name = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",hashes = {sha256 = "50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",hashes = {sha256 = "b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}}, - {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}}, - {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}}, - {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}}, - {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}}, - {name = "numpy-2.3.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl",hashes = {sha256 = "cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}}, - {name = "numpy-2.3.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}}, - {name = "numpy-2.3.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",hashes = {sha256 = "7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",hashes = {sha256 = "533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}}, - {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}}, - {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}}, - {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}}, - {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}}, - {name = "numpy-2.3.3-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl",hashes = {sha256 = "1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}}, - {name = "numpy-2.3.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}}, - {name = "numpy-2.3.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}}, - {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}}, - {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}}, - {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}}, - {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}}, - {name = "numpy-2.3.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl",hashes = {sha256 = "9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}}, - {name = "numpy-2.3.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}}, - {name = "numpy-2.3.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}}, - {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}}, - {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}}, - {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}}, - {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}}, - {name = "numpy-2.3.3-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl",hashes = {sha256 = "a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}}, - {name = "numpy-2.3.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}}, - {name = "numpy-2.3.3-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}}, - {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}}, - {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}}, - {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}}, - {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}}, - {name = "numpy-2.3.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl",hashes = {sha256 = "5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}}, - {name = "numpy-2.3.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}}, - {name = "numpy-2.3.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}}, + {name = "scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5"}}, + {name = "scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925"}}, + {name = "scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9"}}, + {name = "scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7"}}, + {name = "scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb"}}, + {name = "scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e"}}, + {name = "scipy-1.16.2-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl",hashes = {sha256 = "fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c"}}, + {name = "scipy-1.16.2-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl",hashes = {sha256 = "2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104"}}, + {name = "scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl",url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl",hashes = {sha256 = "53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1"}}, + {name = "scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl",hashes = {sha256 = "9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a"}}, + {name = "scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f"}}, + {name = "scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4"}}, + {name = "scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21"}}, + {name = "scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7"}}, + {name = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8"}}, + {name = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472"}}, + {name = "scipy-1.16.2-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351"}}, + {name = "scipy-1.16.2-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d"}}, + {name = "scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/8d/6396e00db1282279a4ddd507c5f5e11f606812b608ee58517ce8abbf883f/scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl",hashes = {sha256 = "89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d"}}, + {name = "scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3b/93/ea9edd7e193fceb8eef149804491890bde73fb169c896b61aa3e2d1e4e77/scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl",hashes = {sha256 = "ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371"}}, + {name = "scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/91/4d/281fddc3d80fd738ba86fd3aed9202331180b01e2c78eaae0642f22f7e83/scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0"}}, + {name = "scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/69/40/b33b74c84606fd301b2915f0062e45733c6ff5708d121dd0deaa8871e2d0/scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232"}}, + {name = "scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/55/a7/22c739e2f21a42cc8f16bc76b47cff4ed54fbe0962832c589591c2abec34/scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1"}}, + {name = "scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/53/11/a0160990b82999b45874dc60c0c183d3a3a969a563fffc476d5a9995c407/scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f"}}, + {name = "scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/96/53/7ef48a4cfcf243c3d0f1643f5887c81f29fdf76911c4e49331828e19fc0a/scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef"}}, + {name = "scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/49/7f/71a69e0afd460049d41c65c630c919c537815277dfea214031005f474d78/scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1"}}, + {name = "scipy-1.16.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/95/20e02ca66fb495a95fba0642fd48e0c390d0ece9b9b14c6e931a60a12dea/scipy-1.16.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e"}}, + {name = "scipy-1.16.2-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/92/ad/13646b9beb0a95528ca46d52b7babafbe115017814a611f2065ee4e61d20/scipy-1.16.2-cp312-cp312-win_arm64.whl",hashes = {sha256 = "2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851"}}, ] -marker = "python_version ~= \"3.12\"" +marker = "python_version ~= \"3.12\" and \"dev\" in extras" [packages.tool.pdm] -dependencies = [] +dependencies = [ + "numpy<2.6,>=1.25.2", +] [[packages]] name = "setuptools" @@ -636,7 +786,7 @@ sdist = {name = "setuptools-80.9.0.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "setuptools-80.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl",hashes = {sha256 = "062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -672,70 +822,6 @@ dependencies = [ "colorama>=0.4.5; sys_platform == \"win32\"", ] -[[packages]] -name = "tiktoken" -version = "0.12.0" -requires-python = ">=3.9" -sdist = {name = "tiktoken-0.12.0.tar.gz", url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hashes = {sha256 = "b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931"}} -wheels = [ - {name = "tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646"}}, - {name = "tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88"}}, - {name = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff"}}, - {name = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830"}}, - {name = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b"}}, - {name = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b"}}, - {name = "tiktoken-0.12.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71"}}, - {name = "tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3"}}, - {name = "tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160"}}, - {name = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa"}}, - {name = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be"}}, - {name = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a"}}, - {name = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3"}}, - {name = "tiktoken-0.12.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f"}}, - {name = "tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8"}}, - {name = "tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b"}}, - {name = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37"}}, - {name = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad"}}, - {name = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5"}}, - {name = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3"}}, - {name = "tiktoken-0.12.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd"}}, - {name = "tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb"}}, - {name = "tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa"}}, - {name = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc"}}, - {name = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded"}}, - {name = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd"}}, - {name = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967"}}, - {name = "tiktoken-0.12.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def"}}, - {name = "tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970"}}, - {name = "tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16"}}, - {name = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030"}}, - {name = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134"}}, - {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a"}}, - {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892"}}, - {name = "tiktoken-0.12.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1"}}, -] -marker = "\"recommended\" in extras" - -[packages.tool.pdm] -dependencies = [ - "regex>=2022.1.18", - "requests>=2.26.0", -] - [[packages]] name = "tox" version = "4.16.0" @@ -819,144 +905,7 @@ wheels = [ {name = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}}, {name = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}}, ] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [] - -[[packages]] -name = "datasets" -version = "4.1.1" -requires-python = ">=3.9.0" -sdist = {name = "datasets-4.1.1.tar.gz", url = "https://files.pythonhosted.org/packages/91/a4/73f8e6ef52c535e1d20d5b2ca83bfe6de399d8b8b8a61ccc8d63d60735aa/datasets-4.1.1.tar.gz", hashes = {sha256 = "7d8d5ba8b12861d2c44bfff9c83484ebfafff1ff553371e5901a8d3aab5450e2"}} -wheels = [ - {name = "datasets-4.1.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/f4/c8/09012ac195a0aab58755800d2efdc0e7d5905053509f12cb5d136c911cda/datasets-4.1.1-py3-none-any.whl",hashes = {sha256 = "62e4f6899a36be9ec74a7e759a6951253cc85b3fcfa0a759b0efa8353b149dac"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [ - "filelock", - "numpy>=1.17", - "pyarrow>=21.0.0", - "dill<0.4.1,>=0.3.0", - "pandas", - "requests>=2.32.2", - "tqdm>=4.66.3", - "xxhash", - "multiprocess<0.70.17", - "fsspec[http]<=2025.9.0,>=2023.1.0", - "huggingface-hub>=0.24.0", - "packaging", - "pyyaml>=5.1", -] - -[[packages]] -name = "eval-type-backport" -version = "0.2.2" -requires-python = ">=3.8" -sdist = {name = "eval_type_backport-0.2.2.tar.gz", url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hashes = {sha256 = "f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1"}} -wheels = [ - {name = "eval_type_backport-0.2.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl",hashes = {sha256 = "cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [] - -[[packages]] -name = "faker" -version = "37.11.0" -requires-python = ">=3.9" -sdist = {name = "faker-37.11.0.tar.gz", url = "https://files.pythonhosted.org/packages/c9/4b/ca43f6bbcef63deb8ac01201af306388670a172587169aab3b192f7490f0/faker-37.11.0.tar.gz", hashes = {sha256 = "22969803849ba0618be8eee2dd01d0d9e2cd3b75e6ff1a291fa9abcdb34da5e6"}} -wheels = [ - {name = "faker-37.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a3/46/8f4097b55e43af39e8e71e1f7aec59ff7398bca54d975c30889bc844719d/faker-37.11.0-py3-none-any.whl",hashes = {sha256 = "1508d2da94dfd1e0087b36f386126d84f8583b3de19ac18e392a2831a6676c57"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [ - "tzdata", -] - -[[packages]] -name = "loguru" -version = "0.7.3" -requires-python = "<4.0,>=3.5" -sdist = {name = "loguru-0.7.3.tar.gz", url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hashes = {sha256 = "19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}} -wheels = [ - {name = "loguru-0.7.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl",hashes = {sha256 = "31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [ - "colorama>=0.3.4; sys_platform == \"win32\"", - "aiocontextvars>=0.2.0; python_version < \"3.7\"", - "win32-setctime>=1.0.0; sys_platform == \"win32\"", -] - -[[packages]] -name = "msgpack" -version = "1.1.2" -requires-python = ">=3.9" -sdist = {name = "msgpack-1.1.2.tar.gz", url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hashes = {sha256 = "3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e"}} -wheels = [ - {name = "msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00"}}, - {name = "msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939"}}, - {name = "msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e"}}, - {name = "msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931"}}, - {name = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014"}}, - {name = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2"}}, - {name = "msgpack-1.1.2-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl",hashes = {sha256 = "80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717"}}, - {name = "msgpack-1.1.2-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl",hashes = {sha256 = "9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b"}}, - {name = "msgpack-1.1.2-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl",hashes = {sha256 = "59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af"}}, - {name = "msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a"}}, - {name = "msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b"}}, - {name = "msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245"}}, - {name = "msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90"}}, - {name = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20"}}, - {name = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27"}}, - {name = "msgpack-1.1.2-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl",hashes = {sha256 = "1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b"}}, - {name = "msgpack-1.1.2-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff"}}, - {name = "msgpack-1.1.2-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46"}}, - {name = "msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf"}}, - {name = "msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7"}}, - {name = "msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999"}}, - {name = "msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e"}}, - {name = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162"}}, - {name = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794"}}, - {name = "msgpack-1.1.2-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl",hashes = {sha256 = "a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c"}}, - {name = "msgpack-1.1.2-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl",hashes = {sha256 = "a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9"}}, - {name = "msgpack-1.1.2-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl",hashes = {sha256 = "e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84"}}, - {name = "msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa"}}, - {name = "msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb"}}, - {name = "msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f"}}, - {name = "msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42"}}, - {name = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9"}}, - {name = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620"}}, - {name = "msgpack-1.1.2-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl",hashes = {sha256 = "1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029"}}, - {name = "msgpack-1.1.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b"}}, - {name = "msgpack-1.1.2-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl",hashes = {sha256 = "be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69"}}, - {name = "msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c"}}, - {name = "msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0"}}, - {name = "msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296"}}, - {name = "msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef"}}, - {name = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c"}}, - {name = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e"}}, - {name = "msgpack-1.1.2-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl",hashes = {sha256 = "602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e"}}, - {name = "msgpack-1.1.2-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl",hashes = {sha256 = "d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68"}}, - {name = "msgpack-1.1.2-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl",hashes = {sha256 = "86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406"}}, - {name = "msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2"}}, - {name = "msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87"}}, - {name = "msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251"}}, - {name = "msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a"}}, - {name = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f"}}, - {name = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f"}}, - {name = "msgpack-1.1.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl",hashes = {sha256 = "e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9"}}, - {name = "msgpack-1.1.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa"}}, -] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1062,8 +1011,239 @@ wheels = [ {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}}, {name = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}}, ] +marker = "\"all\" in extras or \"dev\" in extras or \"vision\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "eval-type-backport" +version = "0.2.2" +requires-python = ">=3.8" +sdist = {name = "eval_type_backport-0.2.2.tar.gz", url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hashes = {sha256 = "f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1"}} +wheels = [ + {name = "eval_type_backport-0.2.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl",hashes = {sha256 = "cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "faker" +version = "37.11.0" +requires-python = ">=3.9" +sdist = {name = "faker-37.11.0.tar.gz", url = "https://files.pythonhosted.org/packages/c9/4b/ca43f6bbcef63deb8ac01201af306388670a172587169aab3b192f7490f0/faker-37.11.0.tar.gz", hashes = {sha256 = "22969803849ba0618be8eee2dd01d0d9e2cd3b75e6ff1a291fa9abcdb34da5e6"}} +wheels = [ + {name = "faker-37.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a3/46/8f4097b55e43af39e8e71e1f7aec59ff7398bca54d975c30889bc844719d/faker-37.11.0-py3-none-any.whl",hashes = {sha256 = "1508d2da94dfd1e0087b36f386126d84f8583b3de19ac18e392a2831a6676c57"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "tzdata", +] + +[[packages]] +name = "loguru" +version = "0.7.3" +requires-python = "<4.0,>=3.5" +sdist = {name = "loguru-0.7.3.tar.gz", url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hashes = {sha256 = "19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}} +wheels = [ + {name = "loguru-0.7.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl",hashes = {sha256 = "31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}}, +] marker = "\"default\" in dependency_groups" +[packages.tool.pdm] +dependencies = [ + "colorama>=0.3.4; sys_platform == \"win32\"", + "aiocontextvars>=0.2.0; python_version < \"3.7\"", + "win32-setctime>=1.0.0; sys_platform == \"win32\"", +] + +[[packages]] +name = "msgpack" +version = "1.1.2" +requires-python = ">=3.9" +sdist = {name = "msgpack-1.1.2.tar.gz", url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hashes = {sha256 = "3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e"}} +wheels = [ + {name = "msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00"}}, + {name = "msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939"}}, + {name = "msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e"}}, + {name = "msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931"}}, + {name = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014"}}, + {name = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2"}}, + {name = "msgpack-1.1.2-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl",hashes = {sha256 = "80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717"}}, + {name = "msgpack-1.1.2-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl",hashes = {sha256 = "9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b"}}, + {name = "msgpack-1.1.2-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl",hashes = {sha256 = "59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af"}}, + {name = "msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a"}}, + {name = "msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b"}}, + {name = "msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245"}}, + {name = "msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90"}}, + {name = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20"}}, + {name = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27"}}, + {name = "msgpack-1.1.2-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl",hashes = {sha256 = "1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b"}}, + {name = "msgpack-1.1.2-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff"}}, + {name = "msgpack-1.1.2-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46"}}, + {name = "msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf"}}, + {name = "msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7"}}, + {name = "msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999"}}, + {name = "msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e"}}, + {name = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162"}}, + {name = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794"}}, + {name = "msgpack-1.1.2-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl",hashes = {sha256 = "a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c"}}, + {name = "msgpack-1.1.2-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl",hashes = {sha256 = "a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9"}}, + {name = "msgpack-1.1.2-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl",hashes = {sha256 = "e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84"}}, + {name = "msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa"}}, + {name = "msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb"}}, + {name = "msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f"}}, + {name = "msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42"}}, + {name = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9"}}, + {name = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620"}}, + {name = "msgpack-1.1.2-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl",hashes = {sha256 = "1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029"}}, + {name = "msgpack-1.1.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b"}}, + {name = "msgpack-1.1.2-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl",hashes = {sha256 = "be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69"}}, + {name = "msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c"}}, + {name = "msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0"}}, + {name = "msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296"}}, + {name = "msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef"}}, + {name = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c"}}, + {name = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e"}}, + {name = "msgpack-1.1.2-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl",hashes = {sha256 = "602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e"}}, + {name = "msgpack-1.1.2-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl",hashes = {sha256 = "d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68"}}, + {name = "msgpack-1.1.2-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl",hashes = {sha256 = "86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406"}}, + {name = "msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2"}}, + {name = "msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87"}}, + {name = "msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251"}}, + {name = "msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a"}}, + {name = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f"}}, + {name = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f"}}, + {name = "msgpack-1.1.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl",hashes = {sha256 = "e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9"}}, + {name = "msgpack-1.1.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "msgspec" +version = "0.19.0" +requires-python = ">=3.9" +sdist = {name = "msgspec-0.19.0.tar.gz", url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hashes = {sha256 = "604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e"}} +wheels = [ + {name = "msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86"}}, + {name = "msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314"}}, + {name = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e"}}, + {name = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5"}}, + {name = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9"}}, + {name = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327"}}, + {name = "msgspec-0.19.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f"}}, + {name = "msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f"}}, + {name = "msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2"}}, + {name = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12"}}, + {name = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc"}}, + {name = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c"}}, + {name = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"}}, + {name = "msgspec-0.19.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"}}, + {name = "msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e"}}, + {name = "msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551"}}, + {name = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7"}}, + {name = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011"}}, + {name = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063"}}, + {name = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716"}}, + {name = "msgspec-0.19.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c"}}, + {name = "msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/40/817282b42f58399762267b30deb8ac011d8db373f8da0c212c85fbe62b8f/msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259"}}, + {name = "msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/92/99/bd7ed738c00f223a8119928661167a89124140792af18af513e6519b0d54/msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36"}}, + {name = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/e5/27/322badde18eb234e36d4a14122b89edd4e2973cdbc3da61ca7edf40a1ccd/msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947"}}, + {name = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/c6/65/080509c5774a1592b2779d902a70b5fe008532759927e011f068145a16cb/msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909"}}, + {name = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6f/2e/1c23c6b4ca6f4285c30a39def1054e2bee281389e4b681b5e3711bd5a8c9/msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a"}}, + {name = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/83/fe/95f9654518879f3359d1e76bc41189113aa9102452170ab7c9a9a4ee52f6/msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633"}}, + {name = "msgspec-0.19.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/79/f6/71ca7e87a1fb34dfe5efea8156c9ef59dd55613aeda2ca562f122cd22012/msgspec-0.19.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90"}}, +] +marker = "\"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "orjson" +version = "3.11.3" +requires-python = ">=3.9" +sdist = {name = "orjson-3.11.3.tar.gz", url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hashes = {sha256 = "1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a"}} +wheels = [ + {name = "orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4"}}, + {name = "orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl",hashes = {sha256 = "bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e"}}, + {name = "orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl",url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl",hashes = {sha256 = "88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d"}}, + {name = "orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl",url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl",hashes = {sha256 = "d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229"}}, + {name = "orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451"}}, + {name = "orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl",hashes = {sha256 = "7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167"}}, + {name = "orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077"}}, + {name = "orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872"}}, + {name = "orjson-3.11.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl",hashes = {sha256 = "0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d"}}, + {name = "orjson-3.11.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804"}}, + {name = "orjson-3.11.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc"}}, + {name = "orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810"}}, + {name = "orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl",hashes = {sha256 = "9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2"}}, + {name = "orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f"}}, + {name = "orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl",hashes = {sha256 = "828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee"}}, + {name = "orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e"}}, + {name = "orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633"}}, + {name = "orjson-3.11.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl",hashes = {sha256 = "2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b"}}, + {name = "orjson-3.11.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae"}}, + {name = "orjson-3.11.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce"}}, + {name = "orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b"}}, + {name = "orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl",hashes = {sha256 = "9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667"}}, + {name = "orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f"}}, + {name = "orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl",hashes = {sha256 = "fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1"}}, + {name = "orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc"}}, + {name = "orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049"}}, + {name = "orjson-3.11.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl",hashes = {sha256 = "3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca"}}, + {name = "orjson-3.11.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1"}}, + {name = "orjson-3.11.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710"}}, + {name = "orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/cd/8b/360674cd817faef32e49276187922a946468579fcaf37afdfb6c07046e92/orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f"}}, + {name = "orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/3d/5fa9ea4b34c1a13be7d9046ba98d06e6feb1d8853718992954ab59d16625/orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl",hashes = {sha256 = "ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/e5/5f/e18367823925e00b1feec867ff5f040055892fc474bf5f7875649ecfa586/orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/0f/bd/3c66b91c4564759cf9f473251ac1650e446c7ba92a7c0f9f56ed54f9f0e6/orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/82/b5/dc8dcd609db4766e2967a85f63296c59d4722b39503e5b0bf7fd340d387f/orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/48/c2/d58ec5fd1270b2aa44c862171891adc2e1241bd7dab26c8f46eb97c6c6f1/orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/73/87/0ef7e22eb8dd1ef940bfe3b9e441db519e692d62ed1aae365406a16d23d0/orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/6a/e5bf7b70883f374710ad74faf99bacfc4b5b5a7797c1d5e130350e0e28a3/orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2"}}, + {name = "orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/bd/0c/4577fd860b6386ffaa56440e792af01c7882b56d2766f55384b5b0e9d39b/orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55"}}, + {name = "orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/66/4b/83e92b2d67e86d1c33f2ea9411742a714a26de63641b082bdbf3d8e481af/orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1"}}, + {name = "orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/6d/e5/9eea6a14e9b5ceb4a271a1fd2e1dec5f2f686755c0fab6673dc6ff3433f4/orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824"}}, + {name = "orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/45/78/8d4f5ad0c80ba9bf8ac4d0fc71f93a7d0dc0844989e645e2074af376c307/orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f"}}, + {name = "orjson-3.11.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/0b/5f/16386970370178d7a9b438517ea3d704efcf163d286422bae3b37b88dbb5/orjson-3.11.3-cp311-cp311-win32.whl",hashes = {sha256 = "6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204"}}, + {name = "orjson-3.11.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/09/60/db16c6f7a41dd8ac9fb651f66701ff2aeb499ad9ebc15853a26c7c152448/orjson-3.11.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b"}}, + {name = "orjson-3.11.3-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/3e/2a/bb811ad336667041dea9b8565c7c9faf2f59b47eb5ab680315eea612ef2e/orjson-3.11.3-cp311-cp311-win_arm64.whl",hashes = {sha256 = "fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e"}}, + {name = "orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/9b/64/4a3cef001c6cd9c64256348d4c13a7b09b857e3e1cbb5185917df67d8ced/orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/10/ce/0c8c87f54f79d051485903dc46226c4d3220b691a151769156054df4562b/orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/ef/d0/249497e861f2d438f45b3ab7b7b361484237414945169aa285608f9f7019/orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/e5/64/00485702f640a0fd56144042a1ea196469f4a3ae93681871564bf74fa996/orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/64/81/110d68dba3909171bf3f05619ad0cf187b430e64045ae4e0aa7ccfe25b15/orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/79/92/dba25c22b0ddfafa1e6516a780a00abac28d49f49e7202eb433a53c3e94e/orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/44/1d/ca2230fd55edbd87b58a43a19032d63a4b180389a97520cc62c535b726f9/orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf"}}, + {name = "orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/b9/96bbc8ed3e47e52b487d504bd6861798977445fbc410da6e87e302dc632d/orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4"}}, + {name = "orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/c4/3c/418fbd93d94b0df71cddf96b7fe5894d64a5d890b453ac365120daec30f7/orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc"}}, + {name = "orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/5b/a9/2bfd58817d736c2f63608dec0c34857339d423eeed30099b126562822191/orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569"}}, + {name = "orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/33/ba/29023771f334096f564e48d82ed855a0ed3320389d6748a9c949e25be734/orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6"}}, + {name = "orjson-3.11.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/39/62/b5a1eca83f54cb3aa11a9645b8a22f08d97dbd13f27f83aae7c6666a0a05/orjson-3.11.3-cp310-cp310-win32.whl",hashes = {sha256 = "bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc"}}, + {name = "orjson-3.11.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e3/c0/7ebfaa327d9a9ed982adc0d9420dbce9a3fec45b60ab32c6308f731333fa/orjson-3.11.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770"}}, +] +marker = "\"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" + [packages.tool.pdm] dependencies = [] @@ -1128,11 +1308,11 @@ dependencies = [ [[packages]] name = "transformers" -version = "4.57.0" +version = "4.57.1" requires-python = ">=3.9.0" -sdist = {name = "transformers-4.57.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/5c/a22c39dac2687f3fe2a6b97e2c1ae516e91cd4d3976a7a2b7c24ff2fae48/transformers-4.57.0.tar.gz", hashes = {sha256 = "d045753f3d93f9216e693cdb168698dfd2e9d3aad1bb72579a5d60ebf1545a8b"}} +sdist = {name = "transformers-4.57.1.tar.gz", url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hashes = {sha256 = "f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55"}} wheels = [ - {name = "transformers-4.57.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e5/2b/4d2708ac1ff5cd708b6548f4c5812d0ae40d1c28591c4c1c762b6dbdef2d/transformers-4.57.0-py3-none-any.whl",hashes = {sha256 = "9d7c6d098c026e40d897e017ed1f481ab803cbac041021dbc6ae6100e4949b55"}}, + {name = "transformers-4.57.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl",hashes = {sha256 = "b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267"}}, ] marker = "\"default\" in dependency_groups" @@ -1171,7 +1351,7 @@ sdist = {name = "httpx-0.28.1.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "httpx-0.28.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl",hashes = {sha256 = "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1189,7 +1369,7 @@ sdist = {name = "httpcore-1.0.9.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "httpcore-1.0.9-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl",hashes = {sha256 = "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1199,101 +1379,105 @@ dependencies = [ [[packages]] name = "pydantic-core" -version = "2.41.1" +version = "2.41.4" requires-python = ">=3.9" -sdist = {name = "pydantic_core-2.41.1.tar.gz", url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hashes = {sha256 = "1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f"}} -wheels = [ - {name = "pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/41/12/cec246429ddfa2778d2d6301eca5362194dc8749ecb19e621f2f65b5090f/pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl",hashes = {sha256 = "05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/20/39/baba47f8d8b87081302498e610aefc37142ce6a1cc98b2ab6b931a162562/pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/50/32/9a3d87cae2c75a5178334b10358d631bd094b916a00a5993382222dbfd92/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/27/42/a96c9d793a04cf2a9773bff98003bb154087b94f5530a2ce6063ecfec583/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/3e/8d/028c4b7d157a005b1f52c086e2d4b0067886b213c86220c1153398dbdf8f/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/08/f7/ee64cda8fcc9ca3f4716e6357144f9ee71166775df582a1b6b738bf6da57/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/c0/e8ec05f0f5ee7a3656973ad9cd3bc73204af99f6512c1a4562f6fb4b3f7d/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/0a/25/d77a73ff24e2e4fcea64472f5e39b0402d836da9b08b5361a734d0153023/pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/66/45/4a4ebaaae12a740552278d06fe71418c0f2869537a369a89c0e6723b341d/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl",hashes = {sha256 = "4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/da/6d/b727ce1022f143194a36593243ff244ed5a1eb3c9122296bf7e716aa37ba/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl",hashes = {sha256 = "3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/8c/02df9d8506c427787059f87c6c7253435c6895e12472a652d9616ee0fc95/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl",hashes = {sha256 = "49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/98/67/0cf429a7d6802536941f430e6e3243f6d4b68f41eeea4b242372f1901794/pydantic_core-2.41.1-cp314-cp314-win32.whl",hashes = {sha256 = "a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/38/60/742fef93de5d085022d2302a6317a2b34dbfe15258e9396a535c8a100ae7/pydantic_core-2.41.1-cp314-cp314-win_amd64.whl",hashes = {sha256 = "1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/31/38/cdd8ccb8555ef7720bd7715899bd6cfbe3c29198332710e1b61b8f5dd8b8/pydantic_core-2.41.1-cp314-cp314-win_arm64.whl",hashes = {sha256 = "4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9"}}, - {name = "pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e7/7e/8ac10ccb047dc0221aa2530ec3c7c05ab4656d4d4bd984ee85da7f3d5525/pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4"}}, - {name = "pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/e4/7d9791efeb9c7d97e7268f8d20e0da24d03438a7fa7163ab58f1073ba968/pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e"}}, - {name = "pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2d/c3/3f6e6b2342ac11ac8cd5cb56e24c7b14afa27c010e82a765ffa5f771884a/pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl",hashes = {sha256 = "70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl",hashes = {sha256 = "4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl",hashes = {sha256 = "968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl",hashes = {sha256 = "fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl",hashes = {sha256 = "a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl",hashes = {sha256 = "ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb"}}, - {name = "pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc"}}, - {name = "pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67"}}, - {name = "pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl",hashes = {sha256 = "db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl",hashes = {sha256 = "bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl",hashes = {sha256 = "2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl",hashes = {sha256 = "80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl",hashes = {sha256 = "83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl",hashes = {sha256 = "c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/f6/a9/ec440f02e57beabdfd804725ef1e38ac1ba00c49854d298447562e119513/pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "4f276a6134fe1fc1daa692642a3eaa2b7b858599c49a7610816388f5e37566a1"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f0/f9/6bc15bacfd8dcfc073a1820a564516d9c12a435a9a332d4cbbfd48828ddd/pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "07588570a805296ece009c59d9a679dc08fab72fb337365afb4f3a14cfbfc176"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/38/8a/d9edcdcdfe80bade17bed424284427c08bea892aaec11438fa52eaeaf79c/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "28527e4b53400cd60ffbd9812ccb2b5135d042129716d71afd7e45bf42b855c0"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/d5/b3/ff225c6d49fba4279de04677c1c876fc3dc6562fd0c53e9bfd66f58c51a8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "46a1c935c9228bad738c8a41de06478770927baedf581d172494ab36a6b96575"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/47/ba/183e8c0be4321314af3fd1ae6bfc7eafdd7a49bdea5da81c56044a207316/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "447ddf56e2b7d28d200d3e9eafa936fe40485744b5a824b67039937580b3cb20"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/57/c5/aab61e94fd02f45c65f1f8c9ec38bb3b33fbf001a1837c74870e97462572/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "63892ead40c1160ac860b5debcc95c95c5a0035e543a8b5a4eac70dd22e995f4"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/4f/3aaa3bd1ea420a15acc42d7d3ccb3b0bbc5444ae2f9dbc1959f8173e16b8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f4a9543ca355e6df8fbe9c83e9faab707701e9103ae857ecb40f1c0cf8b0e94d"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/58/bd/e3975cdebe03ec080ef881648de316c73f2a6be95c14fc4efb2f7bdd0d41/pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "f2611bdb694116c31e551ed82e20e39a90bea9b7ad9e54aaf2d045ad621aa7a1"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/2b/b8/6b7e7217f147d3b3105b57fb1caec3c4f667581affdfaab6d1d277e1f749/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl",hashes = {sha256 = "fecc130893a9b5f7bfe230be1bb8c61fe66a19db8ab704f808cb25a82aad0bc9"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/fe/7b/239c2fe76bd8b7eef9ae2140d737368a3c6fea4fd27f8f6b4cde6baa3ce9/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl",hashes = {sha256 = "1e2df5f8344c99b6ea5219f00fdc8950b8e6f2c422fbc1cc122ec8641fac85a1"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/2e/77a821a67ff0786f2f14856d6bd1348992f695ee90136a145d7a445c1ff6/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "35291331e9d8ed94c257bab6be1cb3a380b5eee570a2784bffc055e18040a2ea"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/fd/9a/b54512bb9df7f64c586b369328c30481229b70ca6a5fcbb90b715e15facf/pydantic_core-2.41.1-cp311-cp311-win32.whl",hashes = {sha256 = "2876a095292668d753f1a868c4a57c4ac9f6acbd8edda8debe4218d5848cf42f"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9d/72/63c9a4f1a5c950e65dd522d7dd67f167681f9d4f6ece3b80085a0329f08f/pydantic_core-2.41.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "b92d6c628e9a338846a28dfe3fcdc1a3279388624597898b105e078cdfc59298"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d8/16/4e2706184209f61b50c231529257c12eb6bd9eb36e99ea1272e4815d2200/pydantic_core-2.41.1-cp311-cp311-win_arm64.whl",hashes = {sha256 = "7d82ae99409eb69d507a89835488fb657faa03ff9968a9379567b0d2e2e56bc5"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/6c/fa3e45c2b054a1e627a89a364917f12cbe3abc3e91b9004edaae16e7b3c5/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "af2385d3f98243fb733862f806c5bb9122e5fba05b373e3af40e3c82d711cef1"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e5/17/7eebc38b4658cc8e6902d0befc26388e4c2a5f2e179c561eeb43e1922c7b/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "6550617a0c2115be56f90c31a5370261d8ce9dbf051c3ed53b51172dd34da696"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/2b/00/9fe640194a1717a464ab861d43595c268830f98cb1e2705aa134b3544b70/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "dc17b6ecf4983d298686014c92ebc955a9f9baf9f57dad4065e7906e7bee6222"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/b2/ad/f4cdfaf483b78ee65362363e73b6b40c48e067078d7b146e8816d5945ad6/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "42ae9352cf211f08b04ea110563d6b1e415878eea5b4c70f6bdb17dca3b932d2"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/cb/c1/18f416d40a10f44e9387497ba449f40fdb1478c61ba05c4b6bdb82300362/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "e82947de92068b0a21681a13dd2102387197092fbe7defcfb8453e0913866506"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/42/30/134c8a921630d8a88d6f905a562495a6421e959a23c19b0f49b660801d67/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "e244c37d5471c9acdcd282890c6c4c83747b77238bfa19429b8473586c907656"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/9c/48/a9263aeaebdec81e941198525b43edb3b44f27cfa4cb8005b8d3eb8dec72/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "1e798b4b304a995110d41ec93653e57975620ccb2842ba9420037985e7d7284e"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1d/62/755d2bd2593f701c5839fc084e9c2c5e2418f460383ad04e3b5d0befc3ca/pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/2c/a5c4640dc7132540109f67fe83b566fbc7512ccf2a068cfa22a243df70c7/pydantic_core-2.41.1-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "e63036298322e9aea1c8b7c0a6c1204d615dbf6ec0668ce5b83ff27f07404a61"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e3/e7/a8694c3454a57842095d69c7a4ab3cf81c3c7b590f052738eabfdfc2e234/pydantic_core-2.41.1-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "241299ca91fc77ef64f11ed909d2d9220a01834e8e6f8de61275c4dd16b7c936"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/9c/58/29f12e65b19c1877a0269eb4f23c5d2267eded6120a7d6762501ab843dc9/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "1ab7e594a2a5c24ab8013a7dc8cfe5f2260e80e490685814122081705c2cf2b0"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/98/26/4e677f2b7ec3fbdd10be6b586a82a814c8ebe3e474024c8df2d4260e564e/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "b054ef1a78519cb934b58e9c90c09e93b837c935dcd907b891f2b265b129eb6e"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/29/50/50614bd906089904d7ca1be3b9ecf08c00a327143d48f1decfdc21b3c302/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "f2ab7d10d0ab2ed6da54c757233eb0f48ebfb4f86e9b88ccecb3f92bbd61a538"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/ea/58/b1e640b4ca559273cca7c28e0fe8891d5d8e9a600f5ab4882670ec107549/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "2757606b7948bb853a27e4040820306eaa0ccb9e8f9f8a0fa40cb674e170f350"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/53/25/cd47df3bfb24350e03835f0950288d1054f1cc9a8023401dabe6d4ff2834/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "cec0e75eb61f606bad0a32f2be87507087514e26e8c73db6cbdb8371ccd27917"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/ec/b4/71b2c77e5df527fbbc1a03e72c3fd96c44cd10d4241a81befef8c12b9fc4/pydantic_core-2.41.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "0234236514f44a5bf552105cfe2543a12f48203397d9d0f866affa569345a5b5"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/aa/08/4b8a50733005865efde284fec45da75fe16a258f706e16323c5ace4004eb/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_aarch64.whl",hashes = {sha256 = "1b974e41adfbb4ebb0f65fc4ca951347b17463d60893ba7d5f7b9bb087c83897"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/83/c3/1037cb603ef2130c210150a51b1710d86825b5c28df54a55750099f91196/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_armv7l.whl",hashes = {sha256 = "248dafb3204136113c383e91a4d815269f51562b6659b756cf3df14eefc7d0bb"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/56/4c/52d111869610e6b1a46e1f1035abcdc94d0655587e39104433a290e9f377/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "678f9d76a91d6bcedd7568bbf6beb77ae8447f85d1aeebaab7e2f0829cfc3a13"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/32/5d/4b435f0b52ab543967761aca66b84ad3f0026e491e57de47693d15d0a8db/pydantic_core-2.41.1-cp310-cp310-win32.whl",hashes = {sha256 = "dff5bee1d21ee58277900692a641925d2dddfde65182c972569b1a276d2ac8fb"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/88/52/31b4deafc1d3cb96d0e7c0af70f0dc05454982d135d07f5117e6336153e8/pydantic_core-2.41.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "5042da12e5d97d215f91567110fdfa2e2595a25f17c19b9ff024f31c34f9b53e"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/d4/31/f403d7ca8352e3e4df352ccacd200f5f7f7fe81cef8e458515f015091625/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "fabcbdb12de6eada8d6e9a759097adb3c15440fafc675b3e94ae5c9cb8d678a0"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6e/b5/334473b6d2810df84db67f03d4f666acacfc538512c2d2a254074fee0889/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "80e97ccfaf0aaf67d55de5085b0ed0d994f57747d9d03f2de5cc9847ca737b08"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/ea/5e/45513e4dc621f47397cfa5fef12ba8fa5e8b1c4c07f2ff2a5fef8ff81b25/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "34df1fe8fea5d332484a763702e8b6a54048a9d4fe6ccf41e34a128238e01f52"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/22/e3/f1797c168e5f52b973bed1c585e99827a22d5e579d1ed57d51bc15b14633/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "421b5595f845842fc093f7250e24ee395f54ca62d494fdde96f43ecf9228ae01"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/bb/e1/24ef4c3b4ab91c21c3a09a966c7d2cffe101058a7bfe5cc8b2c7c7d574e2/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "dce8b22663c134583aaad24827863306a933f576c79da450be3984924e2031d1"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/35/74/70c1e225d67f7ef3fdba02c506d9011efaf734020914920b2aa3d1a45e61/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "300a9c162fea9906cc5c103893ca2602afd84f0ec90d3be36f4cc360125d22e1"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/c8/bf/dd4d21037c8bef0d8cce90a86a3f2dcb011c30086db2a10113c3eea23eba/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "e019167628f6e6161ae7ab9fb70f6d076a0bf0d55aa9b20833f86a320c70dd65"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7e/78/3093b334e9c9796c8236a4701cd2ddef1c56fb0928fe282a10c797644380/pydantic_core-2.41.1-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "13ab9cc2de6f9d4ab645a050ae5aee61a2424ac4d3a16ba23d4c2027705e0301"}}, +sdist = {name = "pydantic_core-2.41.4.tar.gz", url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hashes = {sha256 = "70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}} +wheels = [ + {name = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl",hashes = {sha256 = "e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl",hashes = {sha256 = "c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl",hashes = {sha256 = "b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl",hashes = {sha256 = "6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl",hashes = {sha256 = "5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl",hashes = {sha256 = "557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl",hashes = {sha256 = "3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl",hashes = {sha256 = "85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl",hashes = {sha256 = "7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl",hashes = {sha256 = "285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl",hashes = {sha256 = "f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl",hashes = {sha256 = "ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl",hashes = {sha256 = "d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl",hashes = {sha256 = "f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl",hashes = {sha256 = "ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl",hashes = {sha256 = "3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl",hashes = {sha256 = "f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl",hashes = {sha256 = "84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl",hashes = {sha256 = "9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl",hashes = {sha256 = "d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl",hashes = {sha256 = "833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl",hashes = {sha256 = "7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl",hashes = {sha256 = "37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl",hashes = {sha256 = "09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl",hashes = {sha256 = "711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl",hashes = {sha256 = "6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl",hashes = {sha256 = "e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl",hashes = {sha256 = "1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl",hashes = {sha256 = "0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl",hashes = {sha256 = "a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"}}, ] marker = "\"default\" in dependency_groups" @@ -1310,7 +1494,7 @@ sdist = {name = "packaging-25.0.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "packaging-25.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl",hashes = {sha256 = "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1323,20 +1507,7 @@ sdist = {name = "typing_extensions-4.15.0.tar.gz", url = "https://files.pythonho wheels = [ {name = "typing_extensions-4.15.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl",hashes = {sha256 = "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [] - -[[packages]] -name = "colorama" -version = "0.4.6" -requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -sdist = {name = "colorama-0.4.6.tar.gz", url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hashes = {sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}} -wheels = [ - {name = "colorama-0.4.6-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",hashes = {sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}}, -] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1349,7 +1520,7 @@ sdist = {name = "huggingface_hub-0.35.3.tar.gz", url = "https://files.pythonhost wheels = [ {name = "huggingface_hub-0.35.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/31/a0/651f93d154cb72323358bf2bbae3e642bdb5d2f1bfc874d096f7cb159fa0/huggingface_hub-0.35.3-py3-none-any.whl",hashes = {sha256 = "0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1363,6 +1534,19 @@ dependencies = [ "hf-xet<2.0.0,>=1.1.3; platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"", ] +[[packages]] +name = "colorama" +version = "0.4.6" +requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +sdist = {name = "colorama-0.4.6.tar.gz", url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hashes = {sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}} +wheels = [ + {name = "colorama-0.4.6-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",hashes = {sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "markdown-it-py" version = "3.0.0" @@ -1412,7 +1596,7 @@ sdist = {name = "requests-2.32.5.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "requests-2.32.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl",hashes = {sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1430,7 +1614,7 @@ sdist = {name = "urllib3-2.5.0.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "urllib3-2.5.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl",hashes = {sha256 = "e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1443,7 +1627,7 @@ sdist = {name = "tqdm-4.67.1.tar.gz", url = "https://files.pythonhosted.org/pack wheels = [ {name = "tqdm-4.67.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl",hashes = {sha256 = "26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1465,68 +1649,93 @@ dependencies = [] [[packages]] name = "charset-normalizer" -version = "3.4.3" +version = "3.4.4" requires-python = ">=3.7" -sdist = {name = "charset_normalizer-3.4.3.tar.gz", url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hashes = {sha256 = "6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}} -wheels = [ - {name = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl",hashes = {sha256 = "3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl",hashes = {sha256 = "30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl",hashes = {sha256 = "c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl",hashes = {sha256 = "bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl",hashes = {sha256 = "6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl",hashes = {sha256 = "cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl",hashes = {sha256 = "fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}}, - {name = "charset_normalizer-3.4.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl",hashes = {sha256 = "ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl",hashes = {sha256 = "6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl",hashes = {sha256 = "d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}}, -] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +sdist = {name = "charset_normalizer-3.4.4.tar.gz", url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hashes = {sha256 = "94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}} +wheels = [ + {name = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl",hashes = {sha256 = "da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl",hashes = {sha256 = "47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl",hashes = {sha256 = "2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl",hashes = {sha256 = "799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl",hashes = {sha256 = "f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl",hashes = {sha256 = "8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl",hashes = {sha256 = "de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl",hashes = {sha256 = "554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl",hashes = {sha256 = "c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl",hashes = {sha256 = "362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl",hashes = {sha256 = "9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl",hashes = {sha256 = "542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl",hashes = {sha256 = "5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl",hashes = {sha256 = "d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl",hashes = {sha256 = "af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl",hashes = {sha256 = "5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl",hashes = {sha256 = "a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl",hashes = {sha256 = "376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}}, + {name = "charset_normalizer-3.4.4-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl",hashes = {sha256 = "7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl",hashes = {sha256 = "0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl",hashes = {sha256 = "eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl",hashes = {sha256 = "5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl",hashes = {sha256 = "65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl",hashes = {sha256 = "faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl",hashes = {sha256 = "f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl",hashes = {sha256 = "a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl",hashes = {sha256 = "cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1539,7 +1748,7 @@ sdist = {name = "dill-0.4.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "dill-0.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl",hashes = {sha256 = "44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1565,7 +1774,7 @@ sdist = {name = "filelock-3.20.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "filelock-3.20.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl",hashes = {sha256 = "339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1578,7 +1787,7 @@ sdist = {name = "fsspec-2025.9.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "fsspec-2025.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl",hashes = {sha256 = "530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1692,7 +1901,7 @@ wheels = [ {name = "aiohttp-3.13.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/23/ba/47fd065510a8bfab5d5f6e1d97c0de672447c0a941c5021298bd7210afc3/aiohttp-3.13.0-cp310-cp310-win32.whl",hashes = {sha256 = "3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d"}}, {name = "aiohttp-3.13.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c4/38/f5385cb79afa1f31bcaa3625a9e8d849b782edaeac09f894f46439e006a1/aiohttp-3.13.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1840,7 +2049,7 @@ wheels = [ {name = "multidict-6.7.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7"}}, {name = "multidict-6.7.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1877,7 +2086,7 @@ wheels = [ {name = "hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/3d/ab7109e607ed321afaa690f557a9ada6d6d164ec852fd6bf9979665dc3d6/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f"}}, {name = "hf_xet-1.1.10-cp37-abi3-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl",hashes = {sha256 = "5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045"}}, ] -marker = "(platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") and \"default\" in dependency_groups" +marker = "\"default\" in dependency_groups and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"all\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"audio\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"dev\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"vision\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\")" [packages.tool.pdm] dependencies = [] @@ -1910,13 +2119,13 @@ dependencies = [] [[packages]] name = "idna" -version = "3.10" -requires-python = ">=3.6" -sdist = {name = "idna-3.10.tar.gz", url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hashes = {sha256 = "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}} +version = "3.11" +requires-python = ">=3.8" +sdist = {name = "idna-3.11.tar.gz", url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hashes = {sha256 = "795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}} wheels = [ - {name = "idna-3.10-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl",hashes = {sha256 = "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}}, + {name = "idna-3.11-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl",hashes = {sha256 = "771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2042,7 +2251,7 @@ wheels = [ {name = "regex-2025.9.18-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/59/39/aeb11a4ae68faaec2498512cadae09f2d8a91f1f65730fe62b9bffeea150/regex-2025.9.18-cp310-cp310-win_amd64.whl",hashes = {sha256 = "47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450"}}, {name = "regex-2025.9.18-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/29/04/37f2d3fc334a1031fc2767c9d89cec13c2e72207c7e7f6feae8a47f4e149/regex-2025.9.18-cp310-cp310-win_arm64.whl",hashes = {sha256 = "16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442"}}, ] -marker = "\"default\" in dependency_groups or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2092,11 +2301,11 @@ dependencies = [ [[packages]] name = "virtualenv" -version = "20.34.0" +version = "20.35.3" requires-python = ">=3.8" -sdist = {name = "virtualenv-20.34.0.tar.gz", url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hashes = {sha256 = "44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}} +sdist = {name = "virtualenv-20.35.3.tar.gz", url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hashes = {sha256 = "4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44"}} wheels = [ - {name = "virtualenv-20.34.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl",hashes = {sha256 = "341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}}, + {name = "virtualenv-20.35.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl",hashes = {sha256 = "63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a"}}, ] marker = "\"dev\" in extras" @@ -2254,7 +2463,7 @@ wheels = [ {name = "yarl-1.22.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}}, {name = "yarl-1.22.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -2376,18 +2585,18 @@ wheels = [ {name = "propcache-0.4.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}}, {name = "propcache-0.4.1-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl",hashes = {sha256 = "d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] [[packages]] name = "aiofiles" -version = "24.1.0" -requires-python = ">=3.8" -sdist = {name = "aiofiles-24.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hashes = {sha256 = "22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}} +version = "25.1.0" +requires-python = ">=3.9" +sdist = {name = "aiofiles-25.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hashes = {sha256 = "a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2"}} wheels = [ - {name = "aiofiles-24.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl",hashes = {sha256 = "b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}}, + {name = "aiofiles-25.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl",hashes = {sha256 = "abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695"}}, ] marker = "\"default\" in dependency_groups" @@ -2402,7 +2611,7 @@ sdist = {name = "aiohappyeyeballs-2.6.1.tar.gz", url = "https://files.pythonhost wheels = [ {name = "aiohappyeyeballs-2.6.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl",hashes = {sha256 = "f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2430,7 +2639,7 @@ sdist = {name = "aiosignal-1.4.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "aiosignal-1.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl",hashes = {sha256 = "053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -2558,7 +2767,7 @@ wheels = [ {name = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}}, {name = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2586,7 +2795,7 @@ sdist = {name = "attrs-25.4.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "attrs-25.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl",hashes = {sha256 = "adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2608,11 +2817,11 @@ dependencies = [ [[packages]] name = "cachetools" -version = "6.2.0" +version = "6.2.1" requires-python = ">=3.9" -sdist = {name = "cachetools-6.2.0.tar.gz", url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hashes = {sha256 = "38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32"}} +sdist = {name = "cachetools-6.2.1.tar.gz", url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hashes = {sha256 = "3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201"}} wheels = [ - {name = "cachetools-6.2.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl",hashes = {sha256 = "1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6"}}, + {name = "cachetools-6.2.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl",hashes = {sha256 = "09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701"}}, ] marker = "\"dev\" in extras" @@ -2627,7 +2836,7 @@ sdist = {name = "certifi-2025.10.5.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "certifi-2025.10.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl",hashes = {sha256 = "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2660,101 +2869,76 @@ dependencies = [] [[packages]] name = "coverage" -version = "7.10.7" -requires-python = ">=3.9" -sdist = {name = "coverage-7.10.7.tar.gz", url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hashes = {sha256 = "f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}} -wheels = [ - {name = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}}, - {name = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}}, - {name = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}}, - {name = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}}, - {name = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}}, - {name = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}}, - {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}}, - {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}}, - {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl",hashes = {sha256 = "925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}}, - {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}}, - {name = "coverage-7.10.7-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl",hashes = {sha256 = "b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}}, - {name = "coverage-7.10.7-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl",hashes = {sha256 = "1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}}, - {name = "coverage-7.10.7-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl",hashes = {sha256 = "097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}}, - {name = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}}, - {name = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}}, - {name = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}}, - {name = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}}, - {name = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}}, - {name = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}}, - {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}}, - {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl",hashes = {sha256 = "0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}}, - {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}}, - {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}}, - {name = "coverage-7.10.7-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl",hashes = {sha256 = "67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}}, - {name = "coverage-7.10.7-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}}, - {name = "coverage-7.10.7-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}}, - {name = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}}, - {name = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}}, - {name = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}}, - {name = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}}, - {name = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}}, - {name = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}}, - {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}}, - {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}}, - {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl",hashes = {sha256 = "b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}}, - {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}}, - {name = "coverage-7.10.7-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl",hashes = {sha256 = "dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}}, - {name = "coverage-7.10.7-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl",hashes = {sha256 = "cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}}, - {name = "coverage-7.10.7-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl",hashes = {sha256 = "4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}}, - {name = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}}, - {name = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}}, - {name = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}}, - {name = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}}, - {name = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}}, - {name = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}}, - {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}}, - {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}}, - {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}}, - {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}}, - {name = "coverage-7.10.7-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl",hashes = {sha256 = "2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}}, - {name = "coverage-7.10.7-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}}, - {name = "coverage-7.10.7-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}}, - {name = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}}, - {name = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}}, - {name = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}}, - {name = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}}, - {name = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}}, - {name = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}}, - {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}}, - {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}}, - {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl",hashes = {sha256 = "5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}}, - {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}}, - {name = "coverage-7.10.7-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl",hashes = {sha256 = "77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}}, - {name = "coverage-7.10.7-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl",hashes = {sha256 = "f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}}, - {name = "coverage-7.10.7-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl",hashes = {sha256 = "bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}}, - {name = "coverage-7.10.7-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl",hashes = {sha256 = "f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}}, - {name = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}}, - {name = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}}, - {name = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}}, - {name = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}}, - {name = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}}, - {name = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}}, - {name = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}}, - {name = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}}, - {name = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl",hashes = {sha256 = "88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}}, - {name = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}}, - {name = "coverage-7.10.7-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl",hashes = {sha256 = "972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}}, - {name = "coverage-7.10.7-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl",hashes = {sha256 = "a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}}, - {name = "coverage-7.10.7-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl",hashes = {sha256 = "736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}}, - {name = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}}, - {name = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}}, - {name = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}}, - {name = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}}, - {name = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}}, - {name = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}}, - {name = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}}, - {name = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}}, - {name = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl",hashes = {sha256 = "606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}}, - {name = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}}, - {name = "coverage-7.10.7-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl",hashes = {sha256 = "b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}}, - {name = "coverage-7.10.7-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl",hashes = {sha256 = "3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}}, +version = "7.11.0" +requires-python = ">=3.10" +sdist = {name = "coverage-7.11.0.tar.gz", url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hashes = {sha256 = "167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}} +wheels = [ + {name = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}}, + {name = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl",hashes = {sha256 = "f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}}, + {name = "coverage-7.11.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl",hashes = {sha256 = "bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}}, + {name = "coverage-7.11.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}}, + {name = "coverage-7.11.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}}, + {name = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}}, + {name = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl",hashes = {sha256 = "ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}}, + {name = "coverage-7.11.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl",hashes = {sha256 = "4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}}, + {name = "coverage-7.11.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}}, + {name = "coverage-7.11.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}}, + {name = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}}, + {name = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl",hashes = {sha256 = "8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}}, + {name = "coverage-7.11.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl",hashes = {sha256 = "695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}}, + {name = "coverage-7.11.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}}, + {name = "coverage-7.11.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}}, + {name = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}}, + {name = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}}, + {name = "coverage-7.11.0-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl",hashes = {sha256 = "cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}}, + {name = "coverage-7.11.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}}, + {name = "coverage-7.11.0-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}}, + {name = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"}}, + {name = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl",hashes = {sha256 = "dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"}}, + {name = "coverage-7.11.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl",hashes = {sha256 = "037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"}}, + {name = "coverage-7.11.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"}}, + {name = "coverage-7.11.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"}}, + {name = "coverage-7.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl",hashes = {sha256 = "4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}}, ] marker = "\"dev\" in extras" @@ -2769,7 +2953,7 @@ sdist = {name = "h11-0.16.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "h11-0.16.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl",hashes = {sha256 = "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2789,38 +2973,45 @@ dependencies = [] [[packages]] name = "httptools" -version = "0.6.4" -requires-python = ">=3.8.0" -sdist = {name = "httptools-0.6.4.tar.gz", url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hashes = {sha256 = "4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}} -wheels = [ - {name = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}}, - {name = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}}, - {name = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}}, - {name = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}}, - {name = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}}, - {name = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}}, - {name = "httptools-0.6.4-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl",hashes = {sha256 = "28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}}, - {name = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}}, - {name = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}}, - {name = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}}, - {name = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}}, - {name = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}}, - {name = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}}, - {name = "httptools-0.6.4-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl",hashes = {sha256 = "db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}}, - {name = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}}, - {name = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}}, - {name = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}}, - {name = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}}, - {name = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}}, - {name = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}}, - {name = "httptools-0.6.4-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl",hashes = {sha256 = "288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}}, - {name = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}}, - {name = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}}, - {name = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}}, - {name = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}}, - {name = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}}, - {name = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}}, - {name = "httptools-0.6.4-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}}, +version = "0.7.1" +requires-python = ">=3.9" +sdist = {name = "httptools-0.7.1.tar.gz", url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hashes = {sha256 = "abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9"}} +wheels = [ + {name = "httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl",hashes = {sha256 = "c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270"}}, + {name = "httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3"}}, + {name = "httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1"}}, + {name = "httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b"}}, + {name = "httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60"}}, + {name = "httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca"}}, + {name = "httptools-0.7.1-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl",hashes = {sha256 = "cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96"}}, + {name = "httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3"}}, + {name = "httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca"}}, + {name = "httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c"}}, + {name = "httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66"}}, + {name = "httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346"}}, + {name = "httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650"}}, + {name = "httptools-0.7.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6"}}, + {name = "httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5"}}, + {name = "httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5"}}, + {name = "httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03"}}, + {name = "httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2"}}, + {name = "httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362"}}, + {name = "httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c"}}, + {name = "httptools-0.7.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321"}}, + {name = "httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657"}}, + {name = "httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70"}}, + {name = "httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df"}}, + {name = "httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e"}}, + {name = "httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274"}}, + {name = "httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec"}}, + {name = "httptools-0.7.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb"}}, + {name = "httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78"}}, + {name = "httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4"}}, + {name = "httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05"}}, + {name = "httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed"}}, + {name = "httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a"}}, + {name = "httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b"}}, + {name = "httptools-0.7.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568"}}, ] marker = "\"default\" in dependency_groups" @@ -2861,7 +3052,7 @@ sdist = {name = "jinja2-3.1.6.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "jinja2-3.1.6-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl",hashes = {sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}}, ] -marker = "\"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [ @@ -2991,7 +3182,7 @@ wheels = [ {name = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}}, {name = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}}, ] -marker = "\"recommended\" in extras" +marker = "\"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -3080,7 +3271,7 @@ wheels = [ {name = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}}, {name = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl",hashes = {sha256 = "e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}}, ] -marker = "\"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -3126,7 +3317,7 @@ wheels = [ {name = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl",hashes = {sha256 = "d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}}, {name = "multiprocess-0.70.16-py310-none-any.whl",url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl",hashes = {sha256 = "c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -3146,6 +3337,19 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "networkx" +version = "3.5" +requires-python = ">=3.11" +sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hashes = {sha256 = "d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}} +wheels = [ + {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, +] +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"audio\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "nodeenv" version = "1.9.1" @@ -3201,7 +3405,7 @@ wheels = [ {name = "pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594"}}, {name = "pyarrow-21.0.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -3240,25 +3444,25 @@ wheels = [ {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea"}}, {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe"}}, ] -marker = "\"recommended\" in extras" +marker = "\"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] [[packages]] name = "pyproject-api" -version = "1.9.1" -requires-python = ">=3.9" -sdist = {name = "pyproject_api-1.9.1.tar.gz", url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hashes = {sha256 = "43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335"}} +version = "1.10.0" +requires-python = ">=3.10" +sdist = {name = "pyproject_api-1.10.0.tar.gz", url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hashes = {sha256 = "40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330"}} wheels = [ - {name = "pyproject_api-1.9.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl",hashes = {sha256 = "7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948"}}, + {name = "pyproject_api-1.10.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl",hashes = {sha256 = "8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09"}}, ] marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [ "packaging>=25", - "tomli>=2.2.1; python_version < \"3.11\"", + "tomli>=2.3; python_version < \"3.11\"", ] [[packages]] @@ -3351,6 +3555,33 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "sympy" +version = "1.14.0" +requires-python = ">=3.9" +sdist = {name = "sympy-1.14.0.tar.gz", url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hashes = {sha256 = "d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}} +wheels = [ + {name = "sympy-1.14.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl",hashes = {sha256 = "e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "mpmath<1.4,>=1.1.0", +] + +[[packages]] +name = "mpmath" +version = "1.3.0" +sdist = {name = "mpmath-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hashes = {sha256 = "7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}} +wheels = [ + {name = "mpmath-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl",hashes = {sha256 = "a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "tracerite" version = "1.1.3" @@ -3619,7 +3850,7 @@ sdist = {name = "anyio-4.11.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "anyio-4.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl",hashes = {sha256 = "0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -3637,7 +3868,7 @@ sdist = {name = "sniffio-1.3.1.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "sniffio-1.3.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl",hashes = {sha256 = "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -3724,7 +3955,7 @@ wheels = [ {name = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}}, {name = "pandas-2.3.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -3744,7 +3975,7 @@ sdist = {name = "python-dateutil-2.9.0.post0.tar.gz", url = "https://files.pytho wheels = [ {name = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl",hashes = {sha256 = "a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -3758,7 +3989,7 @@ sdist = {name = "pytz-2025.2.tar.gz", url = "https://files.pythonhosted.org/pack wheels = [ {name = "pytz-2025.2-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl",hashes = {sha256 = "5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -3771,7 +4002,7 @@ sdist = {name = "six-1.17.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "six-1.17.0-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl",hashes = {sha256 = "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -3784,7 +4015,7 @@ sdist = {name = "tzdata-2025.2.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "tzdata-2025.2-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl",hashes = {sha256 = "1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -4055,43 +4286,11 @@ wheels = [ {name = "xxhash-3.6.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb"}}, {name = "xxhash-3.6.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] -[[packages]] -name = "scipy" -version = "1.15.3" -requires-python = ">=3.10" -sdist = {name = "scipy-1.15.3.tar.gz", url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hashes = {sha256 = "eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}} -wheels = [ - {name = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",hashes = {sha256 = "34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",hashes = {sha256 = "3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",hashes = {sha256 = "6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}}, - {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}}, - {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}}, - {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}}, - {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}}, - {name = "scipy-1.15.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",hashes = {sha256 = "ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",hashes = {sha256 = "aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",hashes = {sha256 = "1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}}, - {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}}, - {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}}, - {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}}, - {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}}, - {name = "scipy-1.15.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}}, -] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [ - "numpy<2.5,>=1.23.5", -] - [[packages]] name = "numpy" version = "2.2.6" @@ -4123,11 +4322,43 @@ wheels = [ {name = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}}, {name = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras" +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"audio\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"vision\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "scipy" +version = "1.15.3" +requires-python = ">=3.10" +sdist = {name = "scipy-1.15.3.tar.gz", url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hashes = {sha256 = "eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}} +wheels = [ + {name = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}}, + {name = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",hashes = {sha256 = "34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}}, + {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",hashes = {sha256 = "3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}}, + {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",hashes = {sha256 = "6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}}, + {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}}, + {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}}, + {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}}, + {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}}, + {name = "scipy-1.15.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}}, + {name = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}}, + {name = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",hashes = {sha256 = "ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}}, + {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",hashes = {sha256 = "aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}}, + {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",hashes = {sha256 = "1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}}, + {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}}, + {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}}, + {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}}, + {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}}, + {name = "scipy-1.15.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}}, +] +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "numpy<2.5,>=1.23.5", +] + [[packages]] name = "tomli" version = "2.3.0" @@ -4170,7 +4401,7 @@ sdist = {name = "async_timeout-5.0.1.tar.gz", url = "https://files.pythonhosted. wheels = [ {name = "async_timeout-5.0.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl",hashes = {sha256 = "39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}}, ] -marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\"" +marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"all\" in extras and python_full_version ~= \"3.10.0\" or \"audio\" in extras and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\" or \"vision\" in extras and python_full_version ~= \"3.10.0\"" [packages.tool.pdm] dependencies = [] @@ -4183,7 +4414,7 @@ sdist = {name = "exceptiongroup-1.3.0.tar.gz", url = "https://files.pythonhosted wheels = [ {name = "exceptiongroup-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl",hashes = {sha256 = "4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}}, ] -marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\"" +marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"all\" in extras and python_full_version ~= \"3.10.0\" or \"audio\" in extras and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\" or \"vision\" in extras and python_full_version ~= \"3.10.0\"" [packages.tool.pdm] dependencies = [ @@ -4206,6 +4437,19 @@ dependencies = [ "typing-extensions>=3.6.4; python_version < \"3.8\"", ] +[[packages]] +name = "networkx" +version = "3.4.2" +requires-python = ">=3.10" +sdist = {name = "networkx-3.4.2.tar.gz", url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hashes = {sha256 = "307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}} +wheels = [ + {name = "networkx-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl",hashes = {sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}}, +] +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"audio\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "zipp" version = "3.23.0" @@ -4220,7 +4464,7 @@ marker = "python_full_version >= \"3.10.0\" and python_full_version < \"3.10.2\" dependencies = [] [tool.pdm] -hashes = {sha256 = "624646aafaf5561776673cdfb44330f8a295ed590670600bc9000c6dcdd8019b"} +hashes = {sha256 = "a61aad0c4563f9e4a33622000214136c2a7aa01d28a2e89e220a415039e7e3eb"} strategy = ["inherit_metadata", "static_urls"] [[tool.pdm.targets]] diff --git a/pyproject.toml b/pyproject.toml index 935587d0..b117ee15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,10 +9,18 @@ include = ["*"] [tool.setuptools.package-data] "guidellm.data" = ["*.gz"] +"guidellm.benchmark.scenarios" = ["*.json", "**/*.json"] [tool.pdm] distribution = true +[[tool.pdm.source]] +name = "torch" +type = "find_links" +#url = "https://download.pytorch.org/whl/cpu/torch_stable.html" +url = "https://download.pytorch.org/whl/cpu/torch/" +include_packages = ["torch"] + # ************************************************ # ********** Project Metadata ********** @@ -54,8 +62,7 @@ dependencies = [ "httpx[http2]<1.0.0", "loguru", "msgpack", - "numpy", - "pillow", + "numpy>=2.0.0", "protobuf", "pydantic>=2.11.7", "pydantic-settings>=2.0.0", @@ -64,20 +71,32 @@ dependencies = [ "sanic", "transformers", "uvloop>=0.18", + "torch", ] [project.optional-dependencies] -perf = [ - "orjson", - "msgpack", - "msgspec", - "uvloop", +# Meta Extras +all = ["guidellm[perf,openai,audio,vision]"] +recommended = ["guidellm[perf,openai]"] +# Feature Extras +perf = ["orjson", "msgpack", "msgspec", "uvloop"] +openai = ["tiktoken>=0.11.0", "blobfile>=3.1.0"] +audio = [ + # Lowest version with full torchcodec support + "datasets[audio]>=4.1.0", + # Torchcodec needs specific torch version + "torch==2.9.*", + "torchcodec==0.8", ] -recommended = [ - "tiktoken>=0.11.0", # For OpenAI tokenizer - "blobfile>=3.1.0", # For OpenAI tokenizer +vision = [ + "datasets[vision]", + "pillow", ] +# Dev Tooling dev = [ + # Install all optional dependencies + "guidellm[all]", + # build "build>=1.0.0", "setuptools>=61.0", @@ -118,7 +137,7 @@ dev = [ ] [dependency-groups] -dev = [ "guidellm[dev]" ] +dev = ["guidellm[dev]"] [project.urls] homepage = "https://github.com/vllm-project/guidellm" @@ -159,6 +178,7 @@ module = [ "transformers.*", "setuptools.*", "setuptools_git_versioning.*", + "torchcodec.*" ] ignore_missing_imports = true @@ -167,7 +187,7 @@ ignore_missing_imports = true target-version = "py310" line-length = 88 indent-width = 4 -exclude = ["build", "dist", "env", ".venv"] +exclude = ["build", "dist", "env", ".venv*"] [tool.ruff.format] quote-style = "double" diff --git a/setup.py b/setup.py index 623bad28..d3b92889 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ import os import re from pathlib import Path -from typing import Optional, Union from packaging.version import Version from setuptools import setup @@ -11,7 +10,7 @@ TAG_VERSION_PATTERN = re.compile(r"^v(\d+\.\d+\.\d+)$") -def get_last_version_diff() -> tuple[Version, Optional[str], Optional[int]]: +def get_last_version_diff() -> tuple[Version, str | None, int | None]: """ Get the last version, last tag, and the number of commits since the last tag. If no tags are found, return the last release version and None for the tag/commits. @@ -38,8 +37,8 @@ def get_last_version_diff() -> tuple[Version, Optional[str], Optional[int]]: def get_next_version( - build_type: str, build_iteration: Optional[Union[str, int]] -) -> tuple[Version, Optional[str], int]: + build_type: str, build_iteration: str | int | None +) -> tuple[Version, str | None, int]: """ Get the next version based on the build type and iteration. - build_type == release: take the last version and add a post if build iteration diff --git a/src/guidellm/__init__.py b/src/guidellm/__init__.py index f2206e94..f466073e 100644 --- a/src/guidellm/__init__.py +++ b/src/guidellm/__init__.py @@ -7,6 +7,8 @@ import logging import os +from datasets import config + with ( open(os.devnull, "w") as devnull, # noqa: PTH123 contextlib.redirect_stderr(devnull), @@ -19,6 +21,7 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false" # Silence warnings for tokenizers hf_logging.set_verbosity_error() logging.getLogger("transformers").setLevel(logging.ERROR) + config.USE_AUDIO_DECODE = False from .logger import configure_logger, logger from .settings import ( diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 0a035551..1e9ba96f 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -1,14 +1,12 @@ """ -GuideLLM command-line interface providing benchmarking, dataset preprocessing, and -mock server functionality. +GuideLLM command-line interface entry point. -This module serves as the primary entry point for the GuideLLM CLI application, -offering a comprehensive suite of tools for language model evaluation and testing. -It provides three main command groups: benchmark operations for performance testing -against generative models, dataset preprocessing utilities for data preparation and -transformation, and a mock server for testing and development scenarios. The CLI -supports various backends, output formats, and configuration options to accommodate -different benchmarking needs and deployment environments. +Primary CLI application providing benchmark execution, dataset preprocessing, and +mock server functionality for language model evaluation. Organizes commands into +three main groups: benchmark operations for performance testing, preprocessing +utilities for data transformation, and mock server capabilities for development +and testing. Supports multiple backends, output formats, and flexible configuration +through CLI options and environment variables. Example: :: @@ -28,39 +26,28 @@ import asyncio import codecs from pathlib import Path -from typing import Annotated, Union import click from pydantic import ValidationError try: import uvloop - - HAS_UVLOOP: Annotated[ - bool, "Flag indicating if uvloop is available for event loop optimization" - ] = True except ImportError: uvloop = None - HAS_UVLOOP: Annotated[ - bool, "Flag indicating if uvloop is available for event loop optimization" - ] = False - from guidellm.backends import BackendType from guidellm.benchmark import ( + BenchmarkGenerativeTextArgs, GenerativeConsoleBenchmarkerProgress, - InjectExtrasAggregator, ProfileType, benchmark_generative_text, - reimport_benchmarks_report, -) -from guidellm.benchmark.scenario import ( - GenerativeTextScenario, get_builtin_scenarios, + reimport_benchmarks_report, ) from guidellm.mock_server import MockServer, MockServerConfig from guidellm.preprocess.dataset import ShortPromptStrategy, process_dataset from guidellm.scheduler import StrategyType +from guidellm.schemas import GenerativeRequestType from guidellm.settings import print_config from guidellm.utils import Console, DefaultGroupHandler, get_literal_vals from guidellm.utils import cli as cli_tools @@ -78,23 +65,21 @@ "run", ] -STRATEGY_PROFILE_CHOICES: Annotated[ - list[str], "Available strategy and profile choices for benchmark execution types" -] = list(get_literal_vals(Union[ProfileType, StrategyType])) +STRATEGY_PROFILE_CHOICES: list[str] = list(get_literal_vals(ProfileType | StrategyType)) +"""Available strategy and profile type choices for benchmark execution.""" def decode_escaped_str(_ctx, _param, value): """ Decode escape sequences in Click option values. - Click automatically escapes characters in option values, converting sequences - like "\\n" to "\\\\n". This function properly decodes these escape sequences - to their intended characters for use in CLI options. + Click automatically escapes characters converting sequences like "\\n" to + "\\\\n". This function decodes these sequences to their intended characters. :param _ctx: Click context (unused) :param _param: Click parameter (unused) - :param value: String value to decode escape sequences from - :return: Decoded string with proper escape sequences + :param value: String value to decode + :return: Decoded string with proper escape sequences, or None if input is None :raises click.BadParameter: When escape sequence decoding fails """ if value is None: @@ -108,33 +93,25 @@ def decode_escaped_str(_ctx, _param, value): @click.group() @click.version_option(package_name="guidellm", message="guidellm version: %(version)s") def cli(): - """ - Main entry point for the GuideLLM command-line interface. - - This is the root command group that organizes all GuideLLM CLI functionality - into logical subgroups for benchmarking, preprocessing, configuration, and - mock server operations. - """ + """GuideLLM CLI for benchmarking, preprocessing, and testing language models.""" @cli.group( - help="Commands to run a new benchmark or load a prior one.", + help="Run a benchmark or load a previously saved benchmark report.", cls=DefaultGroupHandler, default="run", ) def benchmark(): - """ - Benchmark command group for running and managing performance tests. - - This command group provides functionality to execute new benchmarks against - generative models and load previously saved benchmark reports for analysis. - Supports various benchmarking strategies, output formats, and backend types. - """ + """Benchmark commands for performance testing generative models.""" @benchmark.command( "run", - help="Run a benchmark against a generative model using the specified arguments.", + help=( + "Run a benchmark against a generative model. " + "Supports multiple backends, data sources, strategies, and output formats. " + "Configuration can be loaded from a scenario file or specified via options." + ), context_settings={"auto_envvar_prefix": "GUIDELLM"}, ) @click.option( @@ -147,188 +124,190 @@ def benchmark(): dir_okay=False, path_type=Path, ), - click.Choice(get_builtin_scenarios()), + click.Choice(get_builtin_scenarios().keys()), ), default=None, help=( - "The name of a builtin scenario or path to a config file. " - "Missing values from the config will use defaults. " - "Options specified on the commandline will override the scenario." + "Builtin scenario name or path to config file. " + "CLI options override scenario settings." ), ) @click.option( "--target", type=str, - help="The target path for the backend to run benchmarks against. For example, http://localhost:8000", + help="Target backend URL (e.g., http://localhost:8000).", ) @click.option( "--data", type=str, + multiple=True, help=( - "The HuggingFace dataset ID, a path to a HuggingFace dataset, " - "a path to a data file csv, json, jsonl, or txt, " - "or a synthetic data config as a json or key=value string." + "HuggingFace dataset ID, path to dataset, path to data file " + "(csv/json/jsonl/txt), or synthetic data config (json/key=value)." ), ) @click.option( "--profile", "--rate-type", # legacy alias "profile", + default=BenchmarkGenerativeTextArgs.get_default("profile"), type=click.Choice(STRATEGY_PROFILE_CHOICES), - help=( - "The type of benchmark to run. " - f"Supported types {', '.join(STRATEGY_PROFILE_CHOICES)}. " - ), + help=f"Benchmark profile type. Options: {', '.join(STRATEGY_PROFILE_CHOICES)}.", ) @click.option( "--rate", - default=GenerativeTextScenario.get_default("rate"), + type=float, + multiple=True, + default=BenchmarkGenerativeTextArgs.get_default("rate"), help=( - "The rates to run the benchmark at. " - "Can be a single number or a comma-separated list of numbers. " - "For rate-type=sweep, this is the number of benchmarks it runs in the sweep. " - "For rate-type=concurrent, this is the number of concurrent requests. " - "For rate-type=async,constant,poisson, this is the rate requests per second. " - "For rate-type=synchronous,throughput, this must not be set." + "Benchmark rate(s) to test. Meaning depends on profile: " + "sweep=number of benchmarks, concurrent=concurrent requests, " + "async/constant/poisson=requests per second." ), ) -@click.option( - "--random-seed", - default=GenerativeTextScenario.get_default("random_seed"), - type=int, - help="The random seed to use for benchmarking to ensure reproducibility.", -) # Backend configuration @click.option( "--backend", "--backend-type", # legacy alias "backend", type=click.Choice(list(get_literal_vals(BackendType))), - default=GenerativeTextScenario.get_default("backend"), - help=( - "The type of backend to use to run requests against. Defaults to 'openai_http'." - f" Supported types: {', '.join(get_literal_vals(BackendType))}" - ), + default=BenchmarkGenerativeTextArgs.get_default("backend"), + help=f"Backend type. Options: {', '.join(get_literal_vals(BackendType))}.", ) @click.option( "--backend-kwargs", "--backend-args", # legacy alias "backend_kwargs", callback=cli_tools.parse_json, - default=GenerativeTextScenario.get_default("backend_kwargs"), - help=( - "A JSON string containing any arguments to pass to the backend as a " - "dict with **kwargs. Headers can be removed by setting their value to " - "null. For example: " - """'{"headers": {"Authorization": null, "Custom-Header": "Custom-Value"}}'""" - ), + default=BenchmarkGenerativeTextArgs.get_default("backend_kwargs"), + help="JSON string of arguments to pass to the backend.", ) @click.option( "--model", - default=GenerativeTextScenario.get_default("model"), + default=BenchmarkGenerativeTextArgs.get_default("model"), type=str, + help="Model ID to benchmark. If not provided, uses first available model.", +) +# Data configuration +@click.option( + "--request-type", + default=BenchmarkGenerativeTextArgs.get_default("data_request_formatter"), + type=click.Choice(list(get_literal_vals(GenerativeRequestType))), help=( - "The ID of the model to benchmark within the backend. " - "If None provided (default), then it will use the first model available." + f"Request type to create for each data sample. " + f"Options: {', '.join(get_literal_vals(GenerativeRequestType))}." ), ) -# Data configuration +@click.option( + "--request-formatter-kwargs", + default=None, + callback=cli_tools.parse_json, + help="JSON string of arguments to pass to the request formatter.", +) @click.option( "--processor", - default=GenerativeTextScenario.get_default("processor"), + default=BenchmarkGenerativeTextArgs.get_default("processor"), type=str, help=( - "The processor or tokenizer to use to calculate token counts for statistics " - "and synthetic data generation. If None provided (default), will load " - "using the model arg, if needed." + "Processor or tokenizer for token count calculations. " + "If not provided, loads from model." ), ) @click.option( "--processor-args", - default=GenerativeTextScenario.get_default("processor_args"), + default=BenchmarkGenerativeTextArgs.get_default("processor_args"), callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the processor constructor " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to the processor constructor.", ) @click.option( "--data-args", - default=GenerativeTextScenario.get_default("data_args"), + multiple=True, + default=BenchmarkGenerativeTextArgs.get_default("data_args"), callback=cli_tools.parse_json, + help="JSON string of arguments to pass to dataset creation.", +) +@click.option( + "--data-samples", + default=BenchmarkGenerativeTextArgs.get_default("data_samples"), + type=int, help=( - "A JSON string containing any arguments to pass to the dataset creation " - "as a dict with **kwargs." + "Number of samples from dataset. -1 (default) uses all samples " + "and dynamically generates more." ), ) +@click.option( + "--data-column-mapper", + default=BenchmarkGenerativeTextArgs.get_default("data_column_mapper"), + callback=cli_tools.parse_json, + help="JSON string of column mappings to apply to the dataset.", +) @click.option( "--data-sampler", - default=GenerativeTextScenario.get_default("data_sampler"), - type=click.Choice(["random"]), - help=( - "The data sampler type to use. 'random' will add a random shuffle on the data. " - "Defaults to None" - ), + default=BenchmarkGenerativeTextArgs.get_default("data_sampler"), + type=click.Choice(["shuffle"]), + help="Data sampler type.", +) +@click.option( + "--data-num-workers", + default=BenchmarkGenerativeTextArgs.get_default("data_num_workers"), + type=int, + help="Number of worker processes for data loading.", +) +@click.option( + "--dataloader_kwargs", + default=BenchmarkGenerativeTextArgs.get_default("dataloader_kwargs"), + callback=cli_tools.parse_json, + help="JSON string of arguments to pass to the dataloader constructor.", +) +@click.option( + "--random-seed", + default=BenchmarkGenerativeTextArgs.get_default("random_seed"), + type=int, + help="Random seed for reproducibility.", ) # Output configuration @click.option( "--output-path", type=click.Path(), - default=Path.cwd(), + default=BenchmarkGenerativeTextArgs.get_default("output_path"), help=( - "The path to save the output formats to, if the format is a file type. " - "If it is a directory, it will save all output formats selected under it. " - "If it is a file, it will save the corresponding output format to that file. " - "Any output formats that were given that do not match the file extension will " - "be saved in the parent directory of the file path. " - "Defaults to the current working directory. " + "Path to save output files. Can be a directory or file. " + "If a file, saves that format; mismatched formats save to parent directory." ), ) @click.option( "--output-formats", multiple=True, type=str, - default=("console", "json"), # ("console", "json", "html", "csv") - help=( - "The output formats to use for the benchmark results. " - "Defaults to console, json, html, and csv where the file formats " - "will be saved at the specified output path." - ), + default=BenchmarkGenerativeTextArgs.get_default("output_formats"), + help="Output formats for results (e.g., console, json, html, csv).", ) @click.option( "--disable-console-outputs", is_flag=True, - help="Set this flag to disable console output", + help="Disable console output.", ) # Updates configuration @click.option( "--disable-progress", is_flag=True, - help="Set this flag to disable progress updates to the console", + help="Disable progress updates to the console.", ) @click.option( "--display-scheduler-stats", is_flag=True, - help="Set this flag to display stats for the processes running the benchmarks", + help="Display scheduler process statistics.", ) # Aggregators configuration -@click.option( - "--output-extras", - callback=cli_tools.parse_json, - help="A JSON string of extra data to save with the output benchmarks", -) @click.option( "--warmup", "--warmup-percent", # legacy alias "warmup", type=float, - default=GenerativeTextScenario.get_default("warmup"), + default=BenchmarkGenerativeTextArgs.get_default("warmup"), help=( - "The specification around the number of requests to run before benchmarking. " - "If within (0, 1), then the percent of requests/time to use for warmup. " - "If >=1, then the number of requests or seconds to use for warmup." - "Whether it's requests/time used is dependent on which constraint is active. " - "Default None for no warmup." + "Warmup specification: if in (0,1) = percent, if >=1 = number of " + "requests/seconds (depends on active constraint)." ), ) @click.option( @@ -336,125 +315,117 @@ def benchmark(): "--cooldown-percent", # legacy alias "cooldown", type=float, - default=GenerativeTextScenario.get_default("cooldown"), + default=BenchmarkGenerativeTextArgs.get_default("cooldown"), help=( - "The specification around the number of requests to run after benchmarking. " - "If within (0, 1), then the percent of requests/time to use for cooldown. " - "If >=1, then the number of requests or seconds to use for cooldown." - "Whether it's requests/time used is dependent on which constraint is active. " - "Default None for no cooldown." + "Cooldown specification: if in (0,1) = percent, if >=1 = number of " + "requests/seconds (depends on active constraint)." ), ) @click.option( - "--request-samples", + "--sample-requests", "--output-sampling", # legacy alias - "request_samples", - default=GenerativeTextScenario.get_default("request_samples"), + "sample_requests", type=int, help=( - "The number of samples for each request status and each benchmark to save " - "in the output file. If None (default), will save all samples. " - "Defaults to 20." + "Number of sample requests per status to save. " + "None (default) saves all, recommended: 20." ), ) # Constraints configuration @click.option( "--max-seconds", type=float, - default=GenerativeTextScenario.get_default("max_seconds"), + default=BenchmarkGenerativeTextArgs.get_default("max_seconds"), help=( - "The maximum number of seconds each benchmark can run for. " - "If None, will run until max_requests or the data is exhausted." + "Maximum seconds per benchmark. " + "If None, runs until max_requests or data exhaustion." ), ) @click.option( "--max-requests", type=int, - default=GenerativeTextScenario.get_default("max_requests"), + default=BenchmarkGenerativeTextArgs.get_default("max_requests"), help=( - "The maximum number of requests each benchmark can run for. " - "If None, will run until max_seconds or the data is exhausted." + "Maximum requests per benchmark. " + "If None, runs until max_seconds or data exhaustion." ), ) @click.option( "--max-errors", type=int, - default=GenerativeTextScenario.get_default("max_errors"), - help="Maximum number of errors allowed before stopping the benchmark", + default=BenchmarkGenerativeTextArgs.get_default("max_errors"), + help="Maximum errors before stopping the benchmark.", ) @click.option( "--max-error-rate", type=float, - default=GenerativeTextScenario.get_default("max_error_rate"), - help="Maximum error rate allowed before stopping the benchmark", + default=BenchmarkGenerativeTextArgs.get_default("max_error_rate"), + help="Maximum error rate before stopping the benchmark.", ) @click.option( "--max-global-error-rate", type=float, - default=GenerativeTextScenario.get_default("max_global_error_rate"), - help="Maximum global error rate allowed across all benchmarks", + default=BenchmarkGenerativeTextArgs.get_default("max_global_error_rate"), + help="Maximum global error rate across all benchmarks.", ) def run(**kwargs): - """ - Execute a generative text benchmark against a target model backend. + request_type = kwargs.pop("request_type", None) + request_formatter_kwargs = kwargs.pop("request_formatter_kwargs", None) + kwargs["data_request_formatter"] = ( + request_type + if not request_formatter_kwargs + else {"request_type": request_type, **request_formatter_kwargs} + ) + kwargs["data"] = cli_tools.format_list_arg( + kwargs.get("data"), default=[], simplify_single=False + ) + kwargs["data_args"] = cli_tools.format_list_arg( + kwargs.get("data_args"), default=[], simplify_single=False + ) + kwargs["rate"] = cli_tools.format_list_arg( + kwargs.get("rate"), default=None, simplify_single=True + ) - Runs comprehensive performance testing using various strategies and profiles, - collecting metrics on latency, throughput, error rates, and resource usage. - Supports multiple backends, data sources, output formats, and constraint types - for flexible benchmark configuration. - """ - scenario = kwargs.pop("scenario") - click_ctx = click.get_current_context() - overrides = cli_tools.set_if_not_default(click_ctx, **kwargs) + disable_console_outputs = kwargs.pop("disable_console_outputs", False) + display_scheduler_stats = kwargs.pop("display_scheduler_stats", False) + disable_progress = kwargs.pop("disable_progress", False) try: - # If a scenario file was specified read from it - if scenario is None: - _scenario = GenerativeTextScenario.model_validate(overrides) - elif isinstance(scenario, Path): - _scenario = GenerativeTextScenario.from_file(scenario, overrides) - else: # Only builtins can make it here; click will catch anything else - _scenario = GenerativeTextScenario.from_builtin(scenario, overrides) - except ValidationError as e: + args = BenchmarkGenerativeTextArgs.create( + scenario=kwargs.pop("scenario", None), **kwargs + ) + except ValidationError as err: # Translate pydantic valdation error to click argument error - errs = e.errors(include_url=False, include_context=True, include_input=True) + errs = err.errors(include_url=False, include_context=True, include_input=True) param_name = "--" + str(errs[0]["loc"][0]).replace("_", "-") raise click.BadParameter( - errs[0]["msg"], ctx=click_ctx, param_hint=param_name - ) from e + errs[0]["msg"], ctx=click.get_current_context(), param_hint=param_name + ) from err - if HAS_UVLOOP: + if uvloop is not None: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) asyncio.run( benchmark_generative_text( - scenario=_scenario, - # Output configuration - output_path=kwargs["output_path"], - output_formats=[ - fmt - for fmt in kwargs["output_formats"] - if not kwargs["disable_console_outputs"] or fmt != "console" - ], - # Updates configuration + args=args, progress=( - [ - GenerativeConsoleBenchmarkerProgress( - display_scheduler_stats=kwargs["display_scheduler_stats"] - ) - ] - if not kwargs["disable_progress"] + GenerativeConsoleBenchmarkerProgress( + display_scheduler_stats=display_scheduler_stats + ) + if not disable_progress else None ), - print_updates=not kwargs["disable_console_outputs"], - # Aggregators configuration - add_aggregators={ - "extras": InjectExtrasAggregator(extras=kwargs["output_extras"]) - }, + console=Console() if not disable_console_outputs else None, ) ) -@benchmark.command("from-file", help="Load a saved benchmark report.") +@benchmark.command( + "from-file", + help=( + "Load a saved benchmark report and optionally re-export to other formats. " + "PATH: Path to the saved benchmark report file (default: ./benchmarks.json)." + ), +) @click.argument( "path", type=click.Path(file_okay=True, dir_okay=False, exists=True), @@ -465,13 +436,9 @@ def run(**kwargs): type=click.Path(), default=Path.cwd(), help=( - "Allows re-exporting the benchmarks to other formats. " - "The path to save the output formats to, if the format is a file type. " - "If it is a directory, it will save all output formats selected under it. " - "If it is a file, it will save the corresponding output format to that file. " - "Any output formats that were given that do not match the file extension will " - "be saved in the parent directory of the file path. " - "Defaults to the current working directory. " + "Directory or file path to save re-exported benchmark results. " + "If a directory, all output formats will be saved there. " + "If a file, the matching format will be saved to that file." ), ) @click.option( @@ -479,57 +446,33 @@ def run(**kwargs): multiple=True, type=str, default=("console", "json"), # ("console", "json", "html", "csv") - help=( - "The output formats to use for the benchmark results. " - "Defaults to console, json, html, and csv where the file formats " - "will be saved at the specified output path." - ), + help="Output formats for benchmark results (e.g., console, json, html, csv).", ) def from_file(path, output_path, output_formats): - """ - Load and optionally re-export a previously saved benchmark report. - - Imports benchmark results from a saved file and provides optional conversion - to different output formats. Supports JSON, YAML, and CSV export formats - based on the output file extension. - """ asyncio.run(reimport_benchmarks_report(path, output_path, output_formats)) @cli.command( - short_help="Prints environment variable settings.", - help=( - "Print out the available configuration settings that can be set " - "through environment variables." - ), + short_help="Show configuration settings.", + help="Display environment variables for configuring GuideLLM behavior.", ) def config(): - """ - Display available GuideLLM configuration environment variables. - - Prints a comprehensive list of all environment variables that can be used - to configure GuideLLM behavior, including their current values, defaults, - and descriptions. - """ print_config() -@cli.group(help="General preprocessing tools and utilities.") +@cli.group(help="Tools for preprocessing datasets for use in benchmarks.") def preprocess(): - """ - Preprocessing command group for dataset preparation and transformation. - - This command group provides utilities for converting, processing, and - optimizing datasets for use in GuideLLM benchmarks. Includes functionality - for token count adjustments, format conversions, and data validation. - """ + """Dataset preprocessing utilities.""" @preprocess.command( + "dataset", help=( - "Convert a dataset to have specific prompt and output token sizes.\n" - "DATA: Path to the input dataset or dataset ID.\n" - "OUTPUT_PATH: Path to save the converted dataset, including file suffix." + "Process a dataset to have specific prompt and output token sizes. " + "Supports multiple strategies for handling prompts and optional " + "Hugging Face Hub upload.\n\n" + "DATA: Path to the input dataset or dataset ID.\n\n" + "OUTPUT_PATH: Path to save the processed dataset, including file suffix." ), context_settings={"auto_envvar_prefix": "GUIDELLM"}, ) @@ -547,81 +490,70 @@ def preprocess(): "--processor", type=str, required=True, - help=( - "The processor or tokenizer to use to calculate token counts for statistics " - "and synthetic data generation." - ), + help="Processor or tokenizer name for calculating token counts.", ) @click.option( "--processor-args", default=None, callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the processor constructor " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to the processor constructor.", ) @click.option( "--data-args", callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the dataset creation " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to dataset creation.", ) @click.option( "--short-prompt-strategy", type=click.Choice([s.value for s in ShortPromptStrategy]), default=ShortPromptStrategy.IGNORE.value, show_default=True, - help="Strategy to handle prompts shorter than the target length. ", + help="Strategy for handling prompts shorter than target length.", ) @click.option( "--pad-char", type=str, default="", callback=decode_escaped_str, - help="The token to pad short prompts with when using the 'pad' strategy.", + help="Character to pad short prompts with when using 'pad' strategy.", ) @click.option( "--concat-delimiter", type=str, default="", help=( - "The delimiter to use when concatenating prompts that are too short." - " Used when strategy is 'concatenate'." + "Delimiter for concatenating short prompts (used with 'concatenate' strategy)." ), ) @click.option( "--prompt-tokens", type=str, default=None, - help="Prompt tokens config (JSON, YAML file or key=value string)", + help="Prompt tokens configuration (JSON, YAML file, or key=value string).", ) @click.option( "--output-tokens", type=str, default=None, - help="Output tokens config (JSON, YAML file or key=value string)", + help="Output tokens configuration (JSON, YAML file, or key=value string).", ) @click.option( "--push-to-hub", is_flag=True, - help="Set this flag to push the converted dataset to the Hugging Face Hub.", + help="Push the processed dataset to Hugging Face Hub.", ) @click.option( "--hub-dataset-id", type=str, default=None, - help="The Hugging Face Hub dataset ID to push to. " - "Required if --push-to-hub is used.", + help=("Hugging Face Hub dataset ID for upload (required if --push-to-hub is set)."), ) @click.option( "--random-seed", type=int, default=42, show_default=True, - help="Random seed for prompt token sampling and output tokens sampling.", + help="Random seed for reproducible token sampling.", ) def dataset( data, @@ -638,13 +570,6 @@ def dataset( hub_dataset_id, random_seed, ): - """ - Convert and process datasets for specific prompt and output token requirements. - - Transforms datasets to meet target token length specifications using various - strategies for handling short prompts and output length adjustments. Supports - multiple input formats and can optionally push results to Hugging Face Hub. - """ process_dataset( data=data, output_path=output_path, @@ -662,71 +587,87 @@ def dataset( ) -@cli.command(help="Start the GuideLLM mock OpenAI/vLLM server for testing.") -@click.option("--host", default="127.0.0.1", help="Host to bind the server to") -@click.option("--port", default=8000, type=int, help="Port to bind the server to") -@click.option("--workers", default=1, type=int, help="Number of worker processes") +@cli.command( + "mock-server", + help=( + "Start a mock OpenAI/vLLM-compatible server for testing. " + "Simulates model inference with configurable latency and token generation." + ), +) +@click.option( + "--host", + default="127.0.0.1", + help="Host address to bind the server to.", +) @click.option( - "--model", default="llama-3.1-8b-instruct", help="The name of the model to mock" + "--port", + default=8000, + type=int, + help="Port number to bind the server to.", +) +@click.option( + "--workers", + default=1, + type=int, + help="Number of worker processes.", +) +@click.option( + "--model", + default="llama-3.1-8b-instruct", + help="Name of the model to mock.", +) +@click.option( + "--processor", + default=None, + help="Processor or tokenizer to use for requests.", ) -@click.option("--processor", default=None, help="The processor to use for requests") @click.option( "--request-latency", default=3, type=float, - help="Request latency in seconds for non-streaming requests", + help="Request latency in seconds for non-streaming requests.", ) @click.option( "--request-latency-std", default=0, type=float, - help=( - "Request latency standard deviation (normal distribution) " - "in seconds for non-streaming requests" - ), + help="Request latency standard deviation in seconds (normal distribution).", ) @click.option( "--ttft-ms", default=150, type=float, - help="Time to first token in milliseconds for streaming requests", + help="Time to first token in milliseconds for streaming requests.", ) @click.option( "--ttft-ms-std", default=0, type=float, - help=( - "Time to first token standard deviation (normal distribution) in milliseconds" - ), + help="Time to first token standard deviation in milliseconds.", ) @click.option( "--itl-ms", default=10, type=float, - help="Inter token latency in milliseconds for streaming requests", + help="Inter-token latency in milliseconds for streaming requests.", ) @click.option( "--itl-ms-std", default=0, type=float, - help=( - "Inter token latency standard deviation (normal distribution) " - "in milliseconds for streaming requests" - ), + help="Inter-token latency standard deviation in milliseconds.", ) @click.option( "--output-tokens", default=128, type=int, - help="Output tokens for streaming requests", + help="Number of output tokens for streaming requests.", ) @click.option( "--output-tokens-std", default=0, type=float, - help=( - "Output tokens standard deviation (normal distribution) for streaming requests" - ), + help="Output tokens standard deviation (normal distribution).", ) def mock_server( host: str, @@ -743,15 +684,6 @@ def mock_server( output_tokens: int, output_tokens_std: float, ): - """ - Start a GuideLLM mock OpenAI/vLLM-compatible server for testing and development. - - Launches a mock server that simulates model inference with configurable latency - characteristics, token generation patterns, and response timing. Useful for - testing GuideLLM benchmarks without requiring actual model deployment or for - development scenarios requiring predictable server behavior. - """ - config = MockServerConfig( host=host, port=port, diff --git a/src/guidellm/backends/__init__.py b/src/guidellm/backends/__init__.py index 064722ac..6577fa72 100644 --- a/src/guidellm/backends/__init__.py +++ b/src/guidellm/backends/__init__.py @@ -1,26 +1,33 @@ """ Backend infrastructure for GuideLLM language model interactions. -Provides abstract base classes, implemented backends, request/response objects, -and timing utilities for standardized communication with LLM providers. +Provides abstract base classes, concrete backend implementations, and response +handlers for standardized communication with generative AI model providers. +The backend system supports distributed execution across worker processes with +pluggable response handlers for different API formats. Key components include +the abstract Backend base class, OpenAI-compatible HTTP backend, and response +handlers for processing streaming and non-streaming API responses. """ -from .backend import ( - Backend, - BackendType, -) -from .objects import ( - GenerationRequest, - GenerationRequestTimings, - GenerationResponse, -) +from __future__ import annotations + +from .backend import Backend, BackendType from .openai import OpenAIHTTPBackend +from .response_handlers import ( + AudioResponseHandler, + ChatCompletionsResponseHandler, + GenerationResponseHandler, + GenerationResponseHandlerFactory, + TextCompletionsResponseHandler, +) __all__ = [ + "AudioResponseHandler", "Backend", "BackendType", - "GenerationRequest", - "GenerationRequestTimings", - "GenerationResponse", + "ChatCompletionsResponseHandler", + "GenerationResponseHandler", + "GenerationResponseHandlerFactory", "OpenAIHTTPBackend", + "TextCompletionsResponseHandler", ] diff --git a/src/guidellm/backends/backend.py b/src/guidellm/backends/backend.py index 8f91d5e7..89169a48 100644 --- a/src/guidellm/backends/backend.py +++ b/src/guidellm/backends/backend.py @@ -2,13 +2,8 @@ Backend interface and registry for generative AI model interactions. Provides the abstract base class for implementing backends that communicate with -generative AI models. Backends handle the lifecycle of generation requests. - -Classes: - Backend: Abstract base class for generative AI backends with registry support. - -Type Aliases: - BackendType: Literal type defining supported backend implementations. +generative AI models. Backends handle the lifecycle of generation requests and +provide a standard interface for distributed execution across worker processes. """ from __future__ import annotations @@ -16,11 +11,8 @@ from abc import abstractmethod from typing import Literal -from guidellm.backends.objects import ( - GenerationRequest, - GenerationResponse, -) from guidellm.scheduler import BackendInterface +from guidellm.schemas import GenerationRequest, GenerationResponse from guidellm.utils import RegistryMixin __all__ = [ @@ -37,11 +29,12 @@ class Backend( BackendInterface[GenerationRequest, GenerationResponse], ): """ - Base class for generative AI backends with registry and lifecycle. + Base class for generative AI backends with registry and lifecycle management. Provides a standard interface for backends that communicate with generative AI models. Combines the registry pattern for automatic discovery with a defined - lifecycle for process-based distributed execution. + lifecycle for process-based distributed execution. Backend state must be + pickleable for distributed execution across process boundaries. Backend lifecycle phases: 1. Creation and configuration @@ -50,9 +43,6 @@ class Backend( 4. Request resolution - Process generation requests 5. Process shutdown - Clean up resources - Backend state (excluding process_startup resources) must be pickleable for - distributed execution across process boundaries. - Example: :: @Backend.register("my_backend") @@ -72,10 +62,10 @@ def create(cls, type_: BackendType, **kwargs) -> Backend: """ Create a backend instance based on the backend type. - :param type_: The type of backend to create. - :param kwargs: Additional arguments for backend initialization. - :return: An instance of a subclass of Backend. - :raises ValueError: If the backend type is not registered. + :param type_: The type of backend to create + :param kwargs: Additional arguments for backend initialization + :return: An instance of a subclass of Backend + :raises ValueError: If the backend type is not registered """ backend = cls.get_registered_object(type_) @@ -92,28 +82,29 @@ def __init__(self, type_: BackendType): """ Initialize a backend instance. - :param type_: The backend type identifier. + :param type_: The backend type identifier """ self.type_ = type_ @property def processes_limit(self) -> int | None: """ - :return: Maximum number of worker processes supported. None if unlimited. + :return: Maximum number of worker processes supported, None if unlimited """ return None @property def requests_limit(self) -> int | None: """ - :return: Maximum number of concurrent requests supported globally. - None if unlimited. + :return: Maximum number of concurrent requests supported globally, + None if unlimited """ return None @abstractmethod async def default_model(self) -> str | None: """ - :return: The default model name or identifier for generation requests. + :return: The default model name or identifier for generation requests, + None if no default model is available """ ... diff --git a/src/guidellm/backends/objects.py b/src/guidellm/backends/objects.py deleted file mode 100644 index 05280940..00000000 --- a/src/guidellm/backends/objects.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Backend object models for request and response handling. - -Provides standardized models for generation requests, responses, and timing -information to ensure consistent data handling across different backend -implementations. -""" - -import uuid -from typing import Any, Literal, Optional - -from pydantic import Field - -from guidellm.scheduler import ( - MeasuredRequestTimings, - SchedulerMessagingPydanticRegistry, -) -from guidellm.utils import StandardBaseModel - -__all__ = [ - "GenerationRequest", - "GenerationRequestTimings", - "GenerationResponse", -] - - -@SchedulerMessagingPydanticRegistry.register() -class GenerationRequest(StandardBaseModel): - """Request model for backend generation operations.""" - - request_id: str = Field( - default_factory=lambda: str(uuid.uuid4()), - description="Unique identifier for the request.", - ) - request_type: Literal["text_completions", "chat_completions"] = Field( - default="text_completions", - description=( - "Type of request. 'text_completions' uses backend.text_completions(), " - "'chat_completions' uses backend.chat_completions()." - ), - ) - content: Any = Field( - description=( - "Request content. For text_completions: string or list of strings. " - "For chat_completions: string, list of messages, or raw content " - "(set raw_content=True in params)." - ) - ) - params: dict[str, Any] = Field( - default_factory=dict, - description=( - "Additional parameters passed to backend methods. " - "Common: max_tokens, temperature, stream." - ), - ) - stats: dict[Literal["prompt_tokens"], int] = Field( - default_factory=dict, - description="Request statistics including prompt token count.", - ) - constraints: dict[Literal["output_tokens"], int] = Field( - default_factory=dict, - description="Request constraints such as maximum output tokens.", - ) - - -@SchedulerMessagingPydanticRegistry.register() -class GenerationResponse(StandardBaseModel): - """Response model for backend generation operations.""" - - request_id: str = Field( - description="Unique identifier matching the original GenerationRequest." - ) - request_args: dict[str, Any] = Field( - description="Arguments passed to the backend for this request." - ) - value: Optional[str] = Field( - default=None, - description="Complete generated text content. None for streaming responses.", - ) - delta: Optional[str] = Field( - default=None, description="Incremental text content for streaming responses." - ) - iterations: int = Field( - default=0, description="Number of generation iterations completed." - ) - request_prompt_tokens: Optional[int] = Field( - default=None, description="Token count from the original request prompt." - ) - request_output_tokens: Optional[int] = Field( - default=None, - description="Expected output token count from the original request.", - ) - response_prompt_tokens: Optional[int] = Field( - default=None, description="Actual prompt token count reported by the backend." - ) - response_output_tokens: Optional[int] = Field( - default=None, description="Actual output token count reported by the backend." - ) - - @property - def prompt_tokens(self) -> Optional[int]: - """ - :return: The number of prompt tokens used in the request - (response_prompt_tokens if available, otherwise request_prompt_tokens). - """ - return self.response_prompt_tokens or self.request_prompt_tokens - - @property - def output_tokens(self) -> Optional[int]: - """ - :return: The number of output tokens generated in the response - (response_output_tokens if available, otherwise request_output_tokens). - """ - return self.response_output_tokens or self.request_output_tokens - - @property - def total_tokens(self) -> Optional[int]: - """ - :return: The total number of tokens used in the request and response. - Sum of prompt_tokens and output_tokens. - """ - if self.prompt_tokens is None or self.output_tokens is None: - return None - return self.prompt_tokens + self.output_tokens - - def preferred_prompt_tokens( - self, preferred_source: Literal["request", "response"] - ) -> Optional[int]: - if preferred_source == "request": - return self.request_prompt_tokens or self.response_prompt_tokens - else: - return self.response_prompt_tokens or self.request_prompt_tokens - - def preferred_output_tokens( - self, preferred_source: Literal["request", "response"] - ) -> Optional[int]: - if preferred_source == "request": - return self.request_output_tokens or self.response_output_tokens - else: - return self.response_output_tokens or self.request_output_tokens - - -@SchedulerMessagingPydanticRegistry.register() -@MeasuredRequestTimings.register("generation_request_timings") -class GenerationRequestTimings(MeasuredRequestTimings): - """Timing model for tracking generation request lifecycle events.""" - - timings_type: Literal["generation_request_timings"] = "generation_request_timings" - first_iteration: Optional[float] = Field( - default=None, - description="Unix timestamp when the first generation iteration began.", - ) - last_iteration: Optional[float] = Field( - default=None, - description="Unix timestamp when the last generation iteration completed.", - ) diff --git a/src/guidellm/backends/openai.py b/src/guidellm/backends/openai.py index ce83076f..1e74fc6e 100644 --- a/src/guidellm/backends/openai.py +++ b/src/guidellm/backends/openai.py @@ -4,42 +4,27 @@ Provides HTTP-based backend for OpenAI-compatible servers including OpenAI API, vLLM servers, and other compatible inference engines. Supports text and chat completions with streaming, authentication, and multimodal capabilities. - -Classes: - UsageStats: Token usage statistics for generation requests. - OpenAIHTTPBackend: HTTP backend for OpenAI-compatible API servers. +Handles request formatting, response parsing, error handling, and token usage +tracking with flexible parameter customization. """ -import base64 -import contextlib -import copy -import json +from __future__ import annotations + +import asyncio import time from collections.abc import AsyncIterator -from pathlib import Path -from typing import Any, ClassVar, Optional, Union +from typing import Any import httpx -from PIL import Image -from pydantic import dataclasses from guidellm.backends.backend import Backend -from guidellm.backends.objects import ( - GenerationRequest, - GenerationRequestTimings, - GenerationResponse, +from guidellm.backends.response_handlers import ( + GenerationResponseHandler, + GenerationResponseHandlerFactory, ) -from guidellm.scheduler import ScheduledRequestInfo - -__all__ = ["OpenAIHTTPBackend", "UsageStats"] - - -@dataclasses.dataclass -class UsageStats: - """Token usage statistics for generation requests.""" +from guidellm.schemas import GenerationRequest, GenerationResponse, RequestInfo - prompt_tokens: Optional[int] = None - output_tokens: Optional[int] = None +__all__ = ["OpenAIHTTPBackend"] @Backend.register("openai_http") @@ -66,109 +51,83 @@ class OpenAIHTTPBackend(Backend): await backend.process_shutdown() """ - HEALTH_PATH: ClassVar[str] = "/health" - MODELS_PATH: ClassVar[str] = "/v1/models" - TEXT_COMPLETIONS_PATH: ClassVar[str] = "/v1/completions" - CHAT_COMPLETIONS_PATH: ClassVar[str] = "/v1/chat/completions" - - MODELS_KEY: ClassVar[str] = "models" - TEXT_COMPLETIONS_KEY: ClassVar[str] = "text_completions" - CHAT_COMPLETIONS_KEY: ClassVar[str] = "chat_completions" - def __init__( self, target: str, - model: Optional[str] = None, - api_key: Optional[str] = None, - organization: Optional[str] = None, - project: Optional[str] = None, + model: str | None = None, + api_routes: dict[str, str] | None = None, + response_handlers: dict[str, Any] | None = None, timeout: float = 60.0, http2: bool = True, follow_redirects: bool = True, - max_output_tokens: Optional[int] = None, - stream_response: bool = True, - extra_query: Optional[dict] = None, - extra_body: Optional[dict] = None, - remove_from_body: Optional[list[str]] = None, - headers: Optional[dict] = None, verify: bool = False, + validate_backend: bool | str | dict[str, Any] = True, ): """ - Initialize OpenAI HTTP backend. - - :param target: Target URL for the OpenAI server (e.g., "http://localhost:8000"). - :param model: Model to use for requests. If None, uses first available model. - :param api_key: API key for authentication. Adds Authorization header - if provided. - :param organization: Organization ID. Adds OpenAI-Organization header - if provided. - :param project: Project ID. Adds OpenAI-Project header if provided. - :param timeout: Request timeout in seconds. Defaults to 60 seconds. - :param http2: Whether to use HTTP/2. Defaults to True. - :param follow_redirects: Whether to follow redirects. Default True. - :param max_output_tokens: Maximum tokens for completions. If None, none is set. - :param stream_response: Whether to stream responses by default. Can be - overridden per request. Defaults to True. - :param extra_query: Additional query parameters. Both general and - endpoint-specific with type keys supported. - :param extra_body: Additional body parameters. Both general and - endpoint-specific with type keys supported. - :param remove_from_body: Parameter names to remove from request bodies. - :param headers: Additional HTTP headers. - :param verify: Whether to verify SSL certificates. Default False. + Initialize OpenAI HTTP backend with server configuration. + + :param target: Base URL of the OpenAI-compatible server + :param model: Model identifier for generation requests + :param api_routes: Custom API endpoint routes mapping + :param response_handlers: Custom response handlers for different request types + :param timeout: Request timeout in seconds + :param http2: Enable HTTP/2 protocol support + :param follow_redirects: Follow HTTP redirects automatically + :param verify: Enable SSL certificate verification + :param validate_backend: Backend validation configuration """ super().__init__(type_="openai_http") # Request Values self.target = target.rstrip("/").removesuffix("/v1") self.model = model - self.headers = self._build_headers(api_key, organization, project, headers) # Store configuration + self.api_routes = api_routes or { + "health": "health", + "models": "v1/models", + "text_completions": "v1/completions", + "chat_completions": "v1/chat/completions", + "audio_transcriptions": "v1/audio/transcriptions", + "audio_translations": "v1/audio/translations", + } + self.response_handlers = response_handlers self.timeout = timeout self.http2 = http2 self.follow_redirects = follow_redirects self.verify = verify - self.max_output_tokens = max_output_tokens - self.stream_response = stream_response - self.extra_query = extra_query or {} - self.extra_body = extra_body or {} - self.remove_from_body = remove_from_body or [] + self.validate_backend: dict[str, Any] | None = self._resolve_validate_kwargs( + validate_backend + ) # Runtime state self._in_process = False - self._async_client: Optional[httpx.AsyncClient] = None + self._async_client: httpx.AsyncClient | None = None @property def info(self) -> dict[str, Any]: """ - :return: Dictionary containing backend configuration details. + Get backend configuration details. + + :return: Dictionary containing backend configuration details """ return { "target": self.target, "model": self.model, - "headers": self.headers, "timeout": self.timeout, "http2": self.http2, "follow_redirects": self.follow_redirects, "verify": self.verify, - "max_output_tokens": self.max_output_tokens, - "stream_response": self.stream_response, - "extra_query": self.extra_query, - "extra_body": self.extra_body, - "remove_from_body": self.remove_from_body, - "health_path": self.HEALTH_PATH, - "models_path": self.MODELS_PATH, - "text_completions_path": self.TEXT_COMPLETIONS_PATH, - "chat_completions_path": self.CHAT_COMPLETIONS_PATH, + "openai_paths": self.api_routes, + "validate_backend": self.validate_backend, } async def process_startup(self): """ Initialize HTTP client and backend resources. - :raises RuntimeError: If backend is already initialized. - :raises httpx.Exception: If HTTP client cannot be created. + :raises RuntimeError: If backend is already initialized + :raises httpx.RequestError: If HTTP client cannot be created """ if self._in_process: raise RuntimeError("Backend already started up for process.") @@ -185,8 +144,8 @@ async def process_shutdown(self): """ Clean up HTTP client and backend resources. - :raises RuntimeError: If backend was not properly initialized. - :raises httpx.Exception: If HTTP client cannot be closed. + :raises RuntimeError: If backend was not properly initialized + :raises httpx.RequestError: If HTTP client cannot be closed """ if not self._in_process: raise RuntimeError("Backend not started up for process.") @@ -197,78 +156,47 @@ async def process_shutdown(self): async def validate(self): """ - Validate backend configuration and connectivity. - - Validate backend configuration and connectivity through test requests, - and auto-selects first available model if none is configured. + Validate backend connectivity and configuration. - :raises RuntimeError: If backend cannot connect or validate configuration. + :raises RuntimeError: If backend cannot connect or validate configuration """ - self._check_in_process() - - if self.model: - with contextlib.suppress(httpx.TimeoutException, httpx.HTTPStatusError): - # Model is set, use /health endpoint as first check - target = f"{self.target}{self.HEALTH_PATH}" - headers = self._get_headers() - response = await self._async_client.get(target, headers=headers) # type: ignore [union-attr] - response.raise_for_status() - - return - - with contextlib.suppress(httpx.TimeoutException, httpx.HTTPStatusError): - # Check if models endpoint is available next - models = await self.available_models() - if models and not self.model: - self.model = models[0] - elif not self.model: - raise RuntimeError( - "No model available and could not set a default model " - "from the server's available models." - ) - - return - - with contextlib.suppress(httpx.TimeoutException, httpx.HTTPStatusError): - # Last check, fall back on dummy request to text completions - async for _, __ in self.text_completions( - prompt="Validate backend", - request_id="validate", - output_token_count=1, - stream_response=False, - ): - pass + if self._async_client is None: + raise RuntimeError("Backend not started up for process.") + if not self.validate_backend: return - raise RuntimeError( - "Backend validation failed. Could not connect to the server or " - "validate the backend configuration." - ) + try: + response = await self._async_client.request(**self.validate_backend) + response.raise_for_status() + except Exception as exc: + raise RuntimeError( + "Backend validation request failed. Could not connect to the server " + "or validate the backend configuration." + ) from exc async def available_models(self) -> list[str]: """ Get available models from the target server. - :return: List of model identifiers. - :raises HTTPError: If models endpoint returns an error. - :raises RuntimeError: If backend is not initialized. + :return: List of model identifiers + :raises httpx.HTTPError: If models endpoint returns an error + :raises RuntimeError: If backend is not initialized """ - self._check_in_process() + if self._async_client is None: + raise RuntimeError("Backend not started up for process.") - target = f"{self.target}{self.MODELS_PATH}" - headers = self._get_headers() - params = self._get_params(self.MODELS_KEY) - response = await self._async_client.get(target, headers=headers, params=params) # type: ignore [union-attr] + target = f"{self.target}/{self.api_routes['models']}" + response = await self._async_client.get(target) response.raise_for_status() return [item["id"] for item in response.json()["data"]] - async def default_model(self) -> Optional[str]: + async def default_model(self) -> str | None: """ Get the default model for this backend. - :return: Model name or None if no model is available. + :return: Model name or None if no model is available """ if self.model or not self._in_process: return self.model @@ -279,371 +207,149 @@ async def default_model(self) -> Optional[str]: async def resolve( self, request: GenerationRequest, - request_info: ScheduledRequestInfo, - history: Optional[list[tuple[GenerationRequest, GenerationResponse]]] = None, - ) -> AsyncIterator[tuple[GenerationResponse, ScheduledRequestInfo]]: + request_info: RequestInfo, + history: list[tuple[GenerationRequest, GenerationResponse]] | None = None, + ) -> AsyncIterator[tuple[GenerationResponse, RequestInfo]]: """ - Process a generation request and yield progressive responses. + Process generation request and yield progressive responses. Handles request formatting, timing tracking, API communication, and response parsing with streaming support. - :param request: Generation request with content and parameters. - :param request_info: Request tracking info updated with timing metadata. - :param history: Conversation history. Currently not supported. - :raises NotImplementedError: If history is provided. - :yields: Tuples of (response, updated_request_info) as generation progresses. + :param request: Generation request with content and parameters + :param request_info: Request tracking info updated with timing metadata + :param history: Conversation history (currently not supported) + :raises NotImplementedError: If history is provided + :raises RuntimeError: If backend is not initialized + :raises ValueError: If request type is unsupported + :yields: Tuples of (response, updated_request_info) as generation progresses """ - self._check_in_process() - if history is not None: - raise NotImplementedError( - "Multi-turn requests with conversation history are not yet supported" - ) + if self._async_client is None: + raise RuntimeError("Backend not started up for process.") - response = GenerationResponse( - request_id=request.request_id, - request_args={ - "request_type": request.request_type, - "output_token_count": request.constraints.get("output_tokens"), - **request.params, - }, - value="", - request_prompt_tokens=request.stats.get("prompt_tokens"), - request_output_tokens=request.constraints.get("output_tokens"), - ) - request_info.request_timings = GenerationRequestTimings() - request_info.request_timings.request_start = time.time() + if history is not None: + raise NotImplementedError("Multi-turn requests not yet supported") - completion_method = ( - self.text_completions - if request.request_type == "text_completions" - else self.chat_completions + response_handler = self._resolve_response_handler( + request_type=request.request_type ) - completion_kwargs = ( + if (request_path := self.api_routes.get(request.request_type)) is None: + raise ValueError(f"Unsupported request type '{request.request_type}'") + request_url = f"{self.target}/{request_path}" + request_files = ( { - "prompt": request.content, - "request_id": request.request_id, - "output_token_count": request.constraints.get("output_tokens"), - "stream_response": request.params.get("stream", self.stream_response), - **request.params, - } - if request.request_type == "text_completions" - else { - "content": request.content, - "request_id": request.request_id, - "output_token_count": request.constraints.get("output_tokens"), - "stream_response": request.params.get("stream", self.stream_response), - **request.params, + key: tuple(value) if isinstance(value, list) else value + for key, value in request.arguments.files.items() } + if request.arguments.files + else None ) - - async for delta, usage_stats in completion_method(**completion_kwargs): - if request_info.request_timings.request_start is None: - request_info.request_timings.request_start = time.time() - - if delta is not None: - if request_info.request_timings.first_iteration is None: - request_info.request_timings.first_iteration = time.time() - response.value += delta # type: ignore [operator] - response.delta = delta - request_info.request_timings.last_iteration = time.time() - response.iterations += 1 - - if usage_stats is not None: - request_info.request_timings.request_end = time.time() - response.response_output_tokens = usage_stats.output_tokens - response.response_prompt_tokens = usage_stats.prompt_tokens - - yield response, request_info - - if request_info.request_timings.request_end is None: - request_info.request_timings.request_end = time.time() - response.delta = None - yield response, request_info - - async def text_completions( - self, - prompt: Union[str, list[str]], - request_id: Optional[str], # noqa: ARG002 - output_token_count: Optional[int] = None, - stream_response: bool = True, - **kwargs, - ) -> AsyncIterator[tuple[Optional[str], Optional[UsageStats]]]: - """ - Generate text completions using the /v1/completions endpoint. - - :param prompt: Text prompt(s) for completion. Single string or list. - :param request_id: Request identifier for tracking. - :param output_token_count: Maximum tokens to generate. Overrides default - if specified. - :param stream_response: Whether to stream response progressively. - :param kwargs: Additional request parameters (temperature, top_p, etc.). - :yields: Tuples of (generated_text, usage_stats). First yield is (None, None). - :raises RuntimeError: If backend is not initialized. - :raises HTTPError: If API request fails. - """ - self._check_in_process() - target = f"{self.target}{self.TEXT_COMPLETIONS_PATH}" - headers = self._get_headers() - params = self._get_params(self.TEXT_COMPLETIONS_KEY) - body = self._get_body( - endpoint_type=self.TEXT_COMPLETIONS_KEY, - request_kwargs=kwargs, - max_output_tokens=output_token_count, - prompt=prompt, - ) - yield None, None # Initial yield for async iterator to signal start - - if not stream_response: - response = await self._async_client.post( # type: ignore [union-attr] - target, - headers=headers, - params=params, - json=body, + request_json = request.arguments.body if not request_files else None + request_data = request.arguments.body if request_files else None + + if not request.arguments.stream: + request_info.timings.request_start = time.time() + response = await self._async_client.request( + request.arguments.method or "POST", + request_url, + params=request.arguments.params, + headers=request.arguments.headers, + json=request_json, + data=request_data, + files=request_files, ) + request_info.timings.request_end = time.time() response.raise_for_status() data = response.json() - yield ( - self._get_completions_text_content(data), - self._get_completions_usage_stats(data), - ) + yield response_handler.compile_non_streaming(request, data), request_info return - body.update({"stream": True, "stream_options": {"include_usage": True}}) - async with self._async_client.stream( # type: ignore [union-attr] - "POST", - target, - headers=headers, - params=params, - json=body, - ) as stream: - stream.raise_for_status() - async for line in stream.aiter_lines(): - if not line or not line.strip().startswith("data:"): - continue - if line.strip() == "data: [DONE]": - break - data = json.loads(line.strip()[len("data: ") :]) - yield ( - self._get_completions_text_content(data), - self._get_completions_usage_stats(data), - ) - - async def chat_completions( - self, - content: Union[ - str, - list[Union[str, dict[str, Union[str, dict[str, str]]], Path, Image.Image]], - Any, - ], - request_id: Optional[str] = None, # noqa: ARG002 - output_token_count: Optional[int] = None, - raw_content: bool = False, - stream_response: bool = True, - **kwargs, - ) -> AsyncIterator[tuple[Optional[str], Optional[UsageStats]]]: - """ - Generate chat completions using the /v1/chat/completions endpoint. - - Supports multimodal inputs including text and images with message formatting. - - :param content: Chat content - string, list of mixed content, or raw content - when raw_content=True. - :param request_id: Request identifier (currently unused). - :param output_token_count: Maximum tokens to generate. Overrides default - if specified. - :param raw_content: If True, passes content directly without formatting. - :param stream_response: Whether to stream response progressively. - :param kwargs: Additional request parameters (temperature, top_p, tools, etc.). - :yields: Tuples of (generated_text, usage_stats). First yield is (None, None). - :raises RuntimeError: If backend is not initialized. - :raises HTTPError: If API request fails. - """ - self._check_in_process() - target = f"{self.target}{self.CHAT_COMPLETIONS_PATH}" - headers = self._get_headers() - params = self._get_params(self.CHAT_COMPLETIONS_KEY) - body = self._get_body( - endpoint_type=self.CHAT_COMPLETIONS_KEY, - request_kwargs=kwargs, - max_output_tokens=output_token_count, - messages=self._get_chat_messages(content) if not raw_content else content, - **kwargs, - ) - yield None, None # Initial yield for async iterator to signal start - - if not stream_response: - response = await self._async_client.post( # type: ignore [union-attr] - target, headers=headers, params=params, json=body - ) - response.raise_for_status() - data = response.json() - yield ( - self._get_completions_text_content(data), - self._get_completions_usage_stats(data), - ) - return - - body.update({"stream": True, "stream_options": {"include_usage": True}}) - async with self._async_client.stream( # type: ignore [union-attr] - "POST", target, headers=headers, params=params, json=body - ) as stream: - stream.raise_for_status() - async for line in stream.aiter_lines(): - if not line or not line.strip().startswith("data:"): - continue - if line.strip() == "data: [DONE]": - break - data = json.loads(line.strip()[len("data: ") :]) - yield ( - self._get_completions_text_content(data), - self._get_completions_usage_stats(data), - ) - - def _build_headers( - self, - api_key: Optional[str], - organization: Optional[str], - project: Optional[str], - user_headers: Optional[dict], - ) -> dict[str, str]: - headers = {} - - if api_key: - headers["Authorization"] = ( - f"Bearer {api_key}" if not api_key.startswith("Bearer") else api_key - ) - if organization: - headers["OpenAI-Organization"] = organization - if project: - headers["OpenAI-Project"] = project - if user_headers: - headers.update(user_headers) - - return {key: val for key, val in headers.items() if val is not None} - - def _check_in_process(self): - if not self._in_process or self._async_client is None: - raise RuntimeError( - "Backend not started up for process, cannot process requests." - ) - - def _get_headers(self) -> dict[str, str]: - return { - "Content-Type": "application/json", - **self.headers, - } + try: + request_info.timings.request_start = time.time() + + async with self._async_client.stream( + request.arguments.method or "POST", + request_url, + params=request.arguments.params, + headers=request.arguments.headers, + json=request_json, + data=request_data, + files=request_files, + ) as stream: + stream.raise_for_status() + end_reached = False + + async for chunk in stream.aiter_lines(): + iter_time = time.time() + + if ( + (iterations := response_handler.add_streaming_line(chunk)) + is None + or iterations < 0 + or end_reached + ): + end_reached = end_reached or iterations is None + continue + + if ( + request_info.timings.first_iteration is None + or request_info.timings.iterations is None + ): + request_info.timings.first_iteration = iter_time + request_info.timings.iterations = 0 + + request_info.timings.last_iteration = iter_time + request_info.timings.iterations += iterations + + request_info.timings.request_end = time.time() + yield response_handler.compile_streaming(request), request_info + except asyncio.CancelledError as err: + # Yield current result to store iterative results before propagating + yield response_handler.compile_streaming(request), request_info + raise err + + def _resolve_validate_kwargs( + self, validate_backend: bool | str | dict[str, Any] + ) -> dict[str, Any] | None: + if not (validate_kwargs := validate_backend): + return None - def _get_params(self, endpoint_type: str) -> dict[str, str]: - if endpoint_type in self.extra_query: - return copy.deepcopy(self.extra_query[endpoint_type]) - return copy.deepcopy(self.extra_query) + if validate_kwargs is True: + validate_kwargs = "health" - def _get_chat_messages( - self, - content: Union[ - str, - list[Union[str, dict[str, Union[str, dict[str, str]]], Path, Image.Image]], - Any, - ], - ) -> list[dict[str, Any]]: - if isinstance(content, str): - return [{"role": "user", "content": content}] - - if not isinstance(content, list): - raise ValueError(f"Unsupported content type: {type(content)}") - - resolved_content = [] - for item in content: - if isinstance(item, dict): - resolved_content.append(item) - elif isinstance(item, str): - resolved_content.append({"type": "text", "text": item}) - elif isinstance(item, (Image.Image, Path)): - resolved_content.append(self._get_chat_message_media_item(item)) - else: - raise ValueError(f"Unsupported content item type: {type(item)}") - - return [{"role": "user", "content": resolved_content}] - - def _get_chat_message_media_item( - self, item: Union[Path, Image.Image] - ) -> dict[str, Any]: - if isinstance(item, Image.Image): - encoded = base64.b64encode(item.tobytes()).decode("utf-8") - return { - "type": "image", - "image": {"url": f"data:image/jpeg;base64,{encoded}"}, - } + if isinstance(validate_kwargs, str) and validate_kwargs in self.api_routes: + validate_kwargs = f"{self.target}/{self.api_routes[validate_kwargs]}" - # Handle file paths - suffix = item.suffix.lower() - if suffix in [".jpg", ".jpeg"]: - image = Image.open(item) - encoded = base64.b64encode(image.tobytes()).decode("utf-8") - return { - "type": "image", - "image": {"url": f"data:image/jpeg;base64,{encoded}"}, - } - elif suffix == ".wav": - encoded = base64.b64encode(item.read_bytes()).decode("utf-8") - return { - "type": "input_audio", - "input_audio": {"data": encoded, "format": "wav"}, + if isinstance(validate_kwargs, str): + validate_kwargs = { + "method": "GET", + "url": validate_kwargs, } - else: - raise ValueError(f"Unsupported file type: {suffix}") - def _get_body( - self, - endpoint_type: str, - request_kwargs: Optional[dict[str, Any]], - max_output_tokens: Optional[int] = None, - **kwargs, - ) -> dict[str, Any]: - # Start with endpoint-specific extra body parameters - extra_body: dict = self.extra_body.get(endpoint_type, self.extra_body) - - body = copy.deepcopy(extra_body) - body.update(request_kwargs or {}) - body.update(kwargs) - body["model"] = self.model - - # Handle token limits - max_tokens = max_output_tokens or self.max_output_tokens - if max_tokens is not None: - body.update( - { - "max_tokens": max_tokens, - "max_completion_tokens": max_tokens, - } + if not isinstance(validate_kwargs, dict) or "url" not in validate_kwargs: + raise ValueError( + "validate_backend must be a boolean, string, or dictionary and contain " + f"a target URL. Got: {validate_kwargs}" ) - # Set stop conditions only for request-level limits - if max_output_tokens: - body.update({"stop": None, "ignore_eos": True}) - if self.remove_from_body: - for key in self.remove_from_body: - body.pop(key, None) + if "method" not in validate_kwargs: + validate_kwargs["method"] = "GET" - return {key: val for key, val in body.items() if val is not None} + return validate_kwargs - def _get_completions_text_content(self, data: dict) -> Optional[str]: - if not data.get("choices"): - return None + def _resolve_response_handler(self, request_type: str) -> GenerationResponseHandler: + if ( + self.response_handlers is not None + and (handler := self.response_handlers.get(request_type)) is not None + ): + return handler - choice: dict = data["choices"][0] - return ( - choice.get("text") - or choice.get("delta", {}).get("content") - or choice.get("message", {}).get("content") + handler_class = GenerationResponseHandlerFactory.get_registered_object( + request_type ) + if handler_class is None: + raise ValueError( + f"No response handler registered for request type '{request_type}'" + ) - def _get_completions_usage_stats(self, data: dict) -> Optional[UsageStats]: - if not data.get("usage"): - return None - - return UsageStats( - prompt_tokens=data["usage"].get("prompt_tokens"), - output_tokens=data["usage"].get("completion_tokens"), - ) + return handler_class() diff --git a/src/guidellm/backends/response_handlers.py b/src/guidellm/backends/response_handlers.py new file mode 100644 index 00000000..b7bd06ad --- /dev/null +++ b/src/guidellm/backends/response_handlers.py @@ -0,0 +1,455 @@ +""" +Response handlers for processing API responses from different generation backends. + +Provides a pluggable system for handling responses from language model backends, +supporting both streaming and non-streaming responses. Each handler implements the +GenerationResponseHandler protocol to parse API responses, extract usage metrics, +and convert them into standardized GenerationResponse objects. +""" + +from __future__ import annotations + +from typing import Any, Protocol + +from guidellm.schemas import GenerationRequest, GenerationResponse, UsageMetrics +from guidellm.utils import RegistryMixin, json + +__all__ = [ + "AudioResponseHandler", + "ChatCompletionsResponseHandler", + "GenerationResponseHandler", + "GenerationResponseHandlerFactory", + "TextCompletionsResponseHandler", +] + + +class GenerationResponseHandler(Protocol): + """ + Protocol for handling generation API responses. + + Defines the interface for processing both streaming and non-streaming responses + from backend APIs, converting them into standardized GenerationResponse objects + with consistent metrics extraction. + """ + + def compile_non_streaming( + self, request: GenerationRequest, response: Any + ) -> GenerationResponse: + """ + Process a complete non-streaming API response. + + :param request: Original generation request + :param response: Raw API response data from the backend + :return: Standardized GenerationResponse with extracted metrics + """ + ... + + def add_streaming_line(self, line: str) -> int | None: + """ + Process a single line from a streaming response. + + :param line: Raw line from the streaming response + :return: 1 if content was updated, 0 if line was ignored, None if done + """ + ... + + def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: + """ + Compile accumulated streaming data into a final response. + + :param request: Original generation request + :return: Standardized GenerationResponse with extracted metrics + """ + ... + + +class GenerationResponseHandlerFactory(RegistryMixin[type[GenerationResponseHandler]]): + """ + Factory for registering and creating response handlers by backend type. + + Registry-based system for associating handler classes with specific backend API + types, enabling automatic selection of the appropriate handler for processing + responses from different generation services. + """ + + +@GenerationResponseHandlerFactory.register("text_completions") +class TextCompletionsResponseHandler(GenerationResponseHandler): + """ + Response handler for OpenAI-style text completion endpoints. + + Processes responses from text completion APIs that return generated text in the + 'choices' array with 'text' fields. Handles both streaming and non-streaming + responses, extracting usage metrics for input and output tokens. + + Example: + :: + handler = TextCompletionsResponseHandler() + response = handler.compile_non_streaming(request, api_response) + """ + + def __init__(self): + """ + Initialize the text completions response handler. + + Sets up internal state for accumulating streaming response data including + text chunks and usage metrics. + """ + self.streaming_texts: list[str] = [] + self.streaming_usage: dict[str, int | dict[str, int]] | None = None + + def compile_non_streaming( + self, request: GenerationRequest, response: dict + ) -> GenerationResponse: + """ + Process a complete text completion response. + + :param request: Original generation request + :param response: Complete API response containing choices and usage data + :return: Standardized GenerationResponse with extracted text and metrics + """ + choices, usage = self.extract_choices_and_usage(response) + input_metrics, output_metrics = self.extract_metrics(usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text=choices[0].get("text", "") if choices else "", + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + def add_streaming_line(self, line: str) -> int | None: + """ + Process a single line from a text completion streaming response. + + Parses Server-Sent Events (SSE) formatted lines, extracting text content + and usage metrics. Accumulates text chunks for final response compilation. + + :param line: Raw SSE line from the streaming response + :return: 1 if text content was extracted, 0 if line ignored, None if done + """ + if not (data := self.extract_line_data(line)): + return None if data is None else 0 + + updated = False + choices, usage = self.extract_choices_and_usage(data) + + if text := choices[0].get("text"): + self.streaming_texts.append(text) + updated = True + + if usage: + self.streaming_usage = usage + + return 1 if updated else 0 + + def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: + """ + Compile accumulated streaming text chunks into a final response. + + :param request: Original generation request + :return: Standardized GenerationResponse with concatenated text and metrics + """ + input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text="".join(self.streaming_texts), + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + def extract_line_data(self, line: str) -> dict[str, Any] | None: + """ + Extract JSON data from a streaming response line. + + :param line: Raw line from the streaming response + :return: Parsed JSON data as dictionary, or None if line indicates completion + """ + if line == "data: [DONE]": + return None + + if not line or not (line := line.strip()) or not line.startswith("data:"): + return {} + + line = line[len("data:") :].strip() + + return json.loads(line) + + def extract_choices_and_usage( + self, response: dict + ) -> tuple[list[dict], dict[str, int | dict[str, int]]]: + """ + Extract choices and usage data from the API response. + + :param response: Complete API response containing choices and usage data + :return: Tuple of choices list and usage dictionary + """ + return response.get("choices", []), response.get("usage", {}) + + def extract_metrics( + self, usage: dict[str, int | dict[str, int]] | None + ) -> tuple[UsageMetrics, UsageMetrics]: + """ + Extract input and output usage metrics from API response usage data. + + :param usage: Usage data dictionary from API response + :return: Tuple of input_metrics and output_metrics as UsageMetrics objects + """ + if not usage: + return UsageMetrics(), UsageMetrics() + + input_details: dict[str, int] = usage.get("prompt_tokens_details", {}) or {} + output_details: dict[str, int] = ( + usage.get("completion_tokens_details", {}) or {} + ) + + return UsageMetrics( + text_tokens=( + input_details.get("prompt_tokens") or usage.get("prompt_tokens") + ), + image_tokens=input_details.get("image_tokens"), + video_tokens=input_details.get("video_tokens"), + audio_tokens=input_details.get("audio_tokens"), + audio_seconds=input_details.get("seconds"), + ), UsageMetrics( + text_tokens=( + output_details.get("completion_tokens") + or usage.get("completion_tokens") + ), + image_tokens=output_details.get("image_tokens"), + video_tokens=output_details.get("video_tokens"), + audio_tokens=output_details.get("audio_tokens"), + audio_seconds=output_details.get("seconds"), + ) + + +@GenerationResponseHandlerFactory.register("chat_completions") +class ChatCompletionsResponseHandler(TextCompletionsResponseHandler): + """ + Response handler for OpenAI-style chat completion endpoints. + + Extends TextCompletionsResponseHandler to handle chat completion responses where + generated text is nested within message objects in the choices array. Processes + both streaming and non-streaming chat completion responses. + """ + + def compile_non_streaming( + self, request: GenerationRequest, response: dict + ) -> GenerationResponse: + """ + Process a complete chat completion response. + + Extracts content from the message object within choices, handling the nested + structure specific to chat completion endpoints. + + :param request: Original generation request + :param response: Complete API response containing choices and usage data + :return: Standardized GenerationResponse with extracted content and metrics + """ + choices, usage = self.extract_choices_and_usage(response) + input_metrics, output_metrics = self.extract_metrics(usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text=(choices[0].get("message", {}).get("content", "") if choices else ""), + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + def add_streaming_line(self, line: str) -> int | None: + """ + Process a single line from a chat completion streaming response. + + Handles the chat completion specific delta structure where content is nested + within delta objects in the streaming response chunks. + + :param line: Raw SSE line from the streaming response + :return: 1 if content was extracted, 0 if line ignored, None if done + """ + if not (data := self.extract_line_data(line)): + return None if data is None else 0 + + updated = False + choices, usage = self.extract_choices_and_usage(data) + + if choices and (content := choices[0].get("delta", {}).get("content")): + self.streaming_texts.append(content) + updated = True + + if usage: + self.streaming_usage = usage + + return 1 if updated else 0 + + def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: + """ + Compile accumulated streaming chat completion content into a final response. + + :param request: Original generation request + :return: Standardized GenerationResponse with concatenated content and metrics + """ + input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text="".join(self.streaming_texts), + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + +@GenerationResponseHandlerFactory.register( + ["audio_transcriptions", "audio_translations"] +) +class AudioResponseHandler: + """ + Response handler for audio transcription and translation endpoints. + + Processes responses from audio processing APIs that convert speech to text, + handling both transcription and translation services. Manages audio-specific + usage metrics including audio tokens and processing duration. + + Example: + :: + handler = AudioResponseHandler() + response = handler.compile_non_streaming(request, api_response) + """ + + def __init__(self): + """ + Initialize the audio response handler. + + Sets up internal state for accumulating streaming response data including + audio buffers, text chunks, and usage metrics. + """ + self.streaming_buffer: bytearray = bytearray() + self.streaming_texts: list[str] = [] + self.streaming_usage: dict[str, int | dict[str, int]] | None = None + + def compile_non_streaming( + self, request: GenerationRequest, response: dict + ) -> GenerationResponse: + """ + Process a complete audio transcription or translation response. + + Extracts transcribed or translated text and audio-specific usage metrics + including processing duration and token counts for audio content. + + :param request: Original generation request + :param response: Complete API response containing text and usage data + :return: Standardized GenerationResponse with extracted text and metrics + """ + usage: dict[str, int | dict[str, int]] = response.get("usage", {}) + input_details: dict[str, int] = usage.get("input_token_details", {}) or {} + output_details: dict[str, int] = usage.get("output_token_details", {}) or {} + text: str = response.get("text", "") + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text=text, + input_metrics=UsageMetrics( + text_tokens=input_details.get("text_tokens", usage.get("input_tokens")), + audio_tokens=input_details.get( + "audio_tokens", usage.get("input_tokens") + ), + audio_seconds=input_details.get("seconds", usage.get("seconds")), + ), + output_metrics=UsageMetrics( + text_tokens=output_details.get( + "text_tokens", usage.get("output_tokens") + ), + ), + ) + + def add_streaming_line(self, line: str) -> int | None: + """ + Process a single line from an audio streaming response. + + Handles JSON-formatted streaming responses from audio processing endpoints, + extracting text content and usage metrics as they become available. + + :param line: Raw JSON line from the streaming response + :return: 1 if text content was extracted, 0 if line ignored, None if done + """ + if line == "data: [DONE]": + return None + + if not line or not (line := line.strip()) or not line.startswith("{"): + return 0 + + data: dict[str, Any] = json.loads(line) + text: str + usage: dict[str, int | dict[str, int]] + updated = False + + if text := data.get("text"): + self.streaming_texts.append(text) + updated = True + + if usage := data.get("usage"): + self.streaming_usage = usage + + return 1 if updated else 0 + + def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: + """ + Compile accumulated streaming audio text into a final response. + + :param request: Original generation request + :return: Standardized GenerationResponse with concatenated text and metrics + """ + input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text="".join(self.streaming_texts), + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + def extract_metrics( + self, usage: dict[str, int | dict[str, int]] | None + ) -> tuple[UsageMetrics, UsageMetrics]: + """ + Extract input and output usage metrics from audio API response usage data. + + Handles audio-specific metrics including processing duration and audio tokens + in addition to standard text token counts. + + :param usage: Usage data dictionary from audio API response + :return: Tuple of input_metrics and output_metrics as UsageMetrics objects + """ + if not usage: + return UsageMetrics(), UsageMetrics() + + input_details: dict[str, int] = usage.get("input_token_details", {}) or {} + output_details: dict[str, int] = usage.get("output_token_details", {}) or {} + + return UsageMetrics( + text_tokens=(input_details.get("text_tokens") or usage.get("input_tokens")), + audio_tokens=( + input_details.get("audio_tokens") or usage.get("audio_tokens") + ), + audio_seconds=(input_details.get("seconds") or usage.get("seconds")), + ), UsageMetrics( + text_tokens=output_details.get("text_tokens") or usage.get("output_tokens"), + ) diff --git a/src/guidellm/benchmark/__init__.py b/src/guidellm/benchmark/__init__.py index 9fdb231d..ef7b2900 100644 --- a/src/guidellm/benchmark/__init__.py +++ b/src/guidellm/benchmark/__init__.py @@ -1,25 +1,17 @@ -from .aggregator import ( - Aggregator, - AggregatorState, - CompilableAggregator, - GenerativeRequestsAggregator, - GenerativeStatsProgressAggregator, - InjectExtrasAggregator, - SchedulerStatsAggregator, - SerializableAggregator, -) +""" +Benchmark execution and performance analysis framework. + +Provides comprehensive benchmarking capabilities for LLM inference workloads, +including profile-based execution strategies, metrics collection and aggregation, +progress tracking, and multi-format output generation. Supports synchronous, +asynchronous, concurrent, sweep, and throughput-based benchmarking profiles for +evaluating model performance under various load conditions. +""" + +from __future__ import annotations + from .benchmarker import Benchmarker from .entrypoints import benchmark_generative_text, reimport_benchmarks_report -from .objects import ( - Benchmark, - BenchmarkMetrics, - BenchmarkSchedulerStats, - BenchmarkT, - GenerativeBenchmark, - GenerativeBenchmarksReport, - GenerativeMetrics, - GenerativeRequestStats, -) from .output import ( GenerativeBenchmarkerConsole, GenerativeBenchmarkerCSV, @@ -35,40 +27,37 @@ SynchronousProfile, ThroughputProfile, ) -from .progress import ( - BenchmarkerProgress, - BenchmarkerProgressGroup, - GenerativeConsoleBenchmarkerProgress, -) -from .scenario import ( - GenerativeTextScenario, - Scenario, - enable_scenarios, - get_builtin_scenarios, -) -from .types import ( - AggregatorInputT, - DataInputT, - OutputFormatT, - ProcessorInputT, - ProgressInputT, +from .progress import BenchmarkerProgress, GenerativeConsoleBenchmarkerProgress +from .scenarios import get_builtin_scenarios +from .schemas import ( + Benchmark, + BenchmarkerArgs, + BenchmarkerDict, + BenchmarkGenerativeTextArgs, + BenchmarkSchedulerStats, + EstimatedBenchmarkState, + GenerativeAudioMetricsSummary, + GenerativeBenchmark, + GenerativeBenchmarksReport, + GenerativeImageMetricsSummary, + GenerativeMetrics, + GenerativeMetricsSummary, + GenerativeVideoMetricsSummary, + SchedulerDict, ) __all__ = [ - "Aggregator", - "AggregatorInputT", - "AggregatorState", "AsyncProfile", "Benchmark", - "BenchmarkMetrics", + "BenchmarkGenerativeTextArgs", "BenchmarkSchedulerStats", - "BenchmarkT", "Benchmarker", + "BenchmarkerArgs", + "BenchmarkerDict", "BenchmarkerProgress", - "BenchmarkerProgressGroup", - "CompilableAggregator", "ConcurrentProfile", - "DataInputT", + "EstimatedBenchmarkState", + "GenerativeAudioMetricsSummary", "GenerativeBenchmark", "GenerativeBenchmarkerCSV", "GenerativeBenchmarkerConsole", @@ -76,25 +65,17 @@ "GenerativeBenchmarkerOutput", "GenerativeBenchmarksReport", "GenerativeConsoleBenchmarkerProgress", + "GenerativeImageMetricsSummary", "GenerativeMetrics", - "GenerativeRequestStats", - "GenerativeRequestsAggregator", - "GenerativeStatsProgressAggregator", - "GenerativeTextScenario", - "InjectExtrasAggregator", - "OutputFormatT", - "ProcessorInputT", + "GenerativeMetricsSummary", + "GenerativeVideoMetricsSummary", "Profile", "ProfileType", - "ProgressInputT", - "Scenario", - "SchedulerStatsAggregator", - "SerializableAggregator", + "SchedulerDict", "SweepProfile", "SynchronousProfile", "ThroughputProfile", "benchmark_generative_text", - "enable_scenarios", "get_builtin_scenarios", "reimport_benchmarks_report", ] diff --git a/src/guidellm/benchmark/aggregator.py b/src/guidellm/benchmark/aggregator.py deleted file mode 100644 index e965c482..00000000 --- a/src/guidellm/benchmark/aggregator.py +++ /dev/null @@ -1,1260 +0,0 @@ -""" -Benchmark result aggregation and compilation interfaces. - -Provides protocols and implementations for collecting, processing, and compiling -benchmark data from scheduler executions into final metrics and statistics. - -Classes: - Aggregator: Protocol for processing benchmark data updates. - CompilableAggregator: Protocol for aggregators that can compile final results. - SchedulerStatsAggregator: Aggregates scheduler timing and performance metrics. - GenerativeRequestsStatsProgressAggregator: Tracks generation metrics during run. - GenerativeRequestsAggregator: Compiles complete generative benchmark results. - -Functions: - add_aggregate_metric: Helper for accumulating timing and count metrics. - -Type Variables: - RequestT: Generic request object type. - ResponseT: Generic response object type. - RequestTimingsT: Generic request timing object type. -""" - -from __future__ import annotations - -import math -import random -from abc import ABC, abstractmethod -from typing import ( - Any, - ClassVar, - Generic, - Literal, - Protocol, - runtime_checkable, -) - -from pydantic import Field, PrivateAttr - -from guidellm.backends import ( - GenerationRequest, - GenerationResponse, -) -from guidellm.benchmark.objects import ( - BenchmarkSchedulerStats, - GenerativeMetrics, - GenerativeRequestStats, -) -from guidellm.scheduler import ( - RequestT, - ResponseT, - ScheduledRequestInfo, - SchedulerState, -) -from guidellm.settings import settings -from guidellm.utils import ( - InfoMixin, - PydanticClassRegistryMixin, - StatusBreakdown, - StatusDistributionSummary, - all_defined, - safe_divide, - safe_getattr, -) - -__all__ = [ - "Aggregator", - "AggregatorState", - "CompilableAggregator", - "GenerativeRequestsAggregator", - "GenerativeStatsProgressAggregator", - "InjectExtrasAggregator", - "SchedulerStatsAggregator", - "SerializableAggregator", -] - - -class AggregatorState(dict[str, Any]): - def add_metric( - self, - key: str, - value: int | float | None, - start_val: int | float | None = 0.0, - count: int | None = 1, - duration: float | None = None, - duration_div: Literal["total", "avg"] = "total", - prefix: str | None = None, - ): - """ - Add timing or count metrics to aggregation state. - """ - if prefix: - self.add_metric( - key=f"{prefix}_{key}", - value=value, - start_val=start_val, - count=count, - duration=duration, - duration_div=duration_div, - ) - return - - if not all_defined(value, start_val, count): - return - - delta_val = value - start_val - self[f"{key}_total"] = self.get(f"{key}_total", 0) + delta_val - self[f"{key}_count"] = self.get(f"{key}_count", 0) + count - self[f"{key}_avg"] = safe_divide( - self.get(f"{key}_total"), self.get(f"{key}_count") - ) - - if all_defined(duration): - self[f"{key}_duration"] = duration - self[f"{key}_rate"] = safe_divide( - self.get(f"{key}_{duration_div}"), duration - ) - - def set_metric( - self, - key: str, - value: int | float | None, - type_: Literal["total", "count", "avg", "duration", "rate"], - prefix: str | None = None, - ): - if prefix: - self.set_metric( - key=f"{prefix}_{key}", - value=value, - type_=type_, - prefix=None, - ) - return - - self[f"{key}_{type_}"] = value - - def get_metric( - self, - key: str, - type_: Literal["total", "count", "avg", "duration", "rate"], - default: int | float | None = None, - prefix: str | None = None, - ) -> int | float | None: - if prefix: - return self.get_metric( - key=f"{prefix}_{key}", - type_=type_, - default=default, - ) - - return self.get(f"{key}_{type_}", default) - - -@runtime_checkable -class Aggregator(Protocol[ResponseT, RequestT]): - """ - Protocol for processing benchmark data updates during execution. - - Defines the interface for aggregators that collect and process request/response - data from scheduler executions. Implementations update aggregation state with - each completed request for eventual compilation into final metrics. - """ - - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Process a completed request and update aggregation state. - - :param state: Current aggregation state to update in-place. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Optional intermediate updates for progress reporting. - """ - - -@runtime_checkable -class CompilableAggregator(Protocol[ResponseT, RequestT]): - """ - Protocol for aggregators that compile final results from aggregated state. - - Extends the Aggregator protocol with the ability to transform accumulated - state into final benchmark results and metrics after execution completes. - """ - - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Process a completed request and update aggregation state. - - :param state: Current aggregation state to update in-place. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Optional intermediate updates for progress reporting. - """ - - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[str, Any]: - """ - Compile aggregated state into final benchmark results. - - :param agg_state: The accumulated aggregation state. - :param scheduler_state: Final scheduler execution state. - :return: Compiled benchmark results and metrics. - """ - - -class SerializableAggregator( - PydanticClassRegistryMixin[type["SerializableAggregator"]], - ABC, - Generic[ResponseT, RequestT], -): - schema_discriminator: ClassVar[str] = "type_" - - @classmethod - def __pydantic_schema_base_type__(cls) -> type[SerializableAggregator]: - if cls.__name__ == "SerializableAggregator": - return cls - - return SerializableAggregator - - @classmethod - @abstractmethod - def validated_kwargs(cls, *args, **kwargs) -> dict[str, Any]: - """ - Validate and process arguments for constraint creation. - - Must be implemented by subclasses to handle their specific parameter patterns. - - :param args: Positional arguments passed to the constraint - :param kwargs: Keyword arguments passed to the constraint - :return: Validated dictionary of parameters for constraint creation - :raises NotImplementedError: Must be implemented by subclasses - """ - ... - - @classmethod - def resolve( - cls, - aggregators: dict[ - str, - Any | dict[str, Any] | Aggregator | CompilableAggregator, - ], - ) -> dict[str, Aggregator | CompilableAggregator]: - """ - Resolve mixed aggregator specifications to callable aggregators. - - :param aggregators: Dictionary mapping aggregator keys to specifications - :return: Dictionary mapping aggregator keys to callable functions - :raises ValueError: If any key is not registered in the factory - """ - resolved = {} - - for key, val in aggregators.items(): - if isinstance(val, (Aggregator, CompilableAggregator)): - resolved[key] = val - else: - aggregator_class = cls.get_registered_object(key) - kwargs = aggregator_class.validated_kwargs(**val) - resolved[key] = aggregator_class(**kwargs) - - return resolved - - type_: Literal["aggregator"] = Field(default="aggregator", description="") - - @abstractmethod - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Process a completed request and update aggregation state. - - :param agg_state: Current aggregation state to update in-place. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Optional intermediate updates for progress reporting. - """ - - @abstractmethod - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[str, Any]: - """ - Compile aggregated state into final benchmark results. - - :param agg_state: The accumulated aggregation state. - :param scheduler_state: Final scheduler execution state. - :return: Compiled benchmark results and metrics. - """ - - -@SerializableAggregator.register("inject_extras") -class InjectExtrasAggregator(SerializableAggregator[ResponseT, RequestT], InfoMixin): - """ - Aggregator for injecting extra metadata into the output. - """ - - @classmethod - def validated_kwargs(cls, extras: dict[str, Any], **_kwargs) -> dict[str, Any]: - return {"extras": extras} - - type_: Literal["inject_extras"] = Field(default="inject_extras") - extras: dict[str, Any] | None = Field(default_factory=None) - - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Inject extra metadata into the aggregation state. - - :param agg_state: Current aggregation state to update. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Updated aggregation state with injected extras. - """ - _ = (state, response, request, request_info, scheduler_state) # unused - return None - - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[str, Any]: - _ = (state, scheduler_state) # unused - return {"extras": self.extras} if self.extras else {} - - -@SerializableAggregator.register("scheduler_stats") -class SchedulerStatsAggregator(SerializableAggregator[ResponseT, RequestT], InfoMixin): - """ - Aggregates scheduler timing and performance metrics. - - Collects timing data for various scheduler phases including queuing, - resolution, and processing delays to generate performance statistics. - """ - - @classmethod - def validated_kwargs(cls, *_args, **_kwargs) -> dict[str, Any]: - return {} - - type_: Literal["scheduler_stats"] = Field(default="scheduler_stats") - - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Aggregate scheduler timing metrics for a completed request. - - :param agg_state: Current aggregation state to update. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Updated aggregation state for intermediate reporting. - """ - _ = (response, request, scheduler_state) # unused - if request_info.status not in ("completed", "errored", "cancelled"): - # Only compile scheduler stats for processed requests - return None - - state["updated_scheduler_stats"] = True - state.add_metric( - key="queued_time", - value=request_info.scheduler_timings.dequeued, - start_val=request_info.scheduler_timings.queued, - ) - state.add_metric( - key="worker_resolve_start_delay", - value=request_info.scheduler_timings.resolve_start, - start_val=request_info.scheduler_timings.scheduled_at, - ) - state.add_metric( - key="worker_resolve_time", - value=request_info.scheduler_timings.resolve_end, - start_val=request_info.scheduler_timings.resolve_start, - ) - state.add_metric( - key="worker_resolve_end_delay", - value=request_info.scheduler_timings.resolve_end, - start_val=safe_getattr(request_info.request_timings, "request_end"), - ) - state.add_metric( - key="finalized_delay", - value=request_info.scheduler_timings.finalized, - start_val=request_info.scheduler_timings.resolve_end, - ) - state.add_metric( - key="worker_targeted_start_delay", - value=request_info.scheduler_timings.resolve_start, - start_val=request_info.scheduler_timings.targeted_start, - ) - state.add_metric( - key="request_start_delay", - value=request_info.scheduler_timings.resolve_start, - start_val=safe_getattr(request_info.request_timings, "request_start"), - ) - state.add_metric( - key="request_time", - value=safe_getattr(request_info.request_timings, "request_end"), - start_val=safe_getattr(request_info.request_timings, "request_start"), - ) - state.add_metric( - key="request_targeted_start_delay", - value=safe_getattr(request_info.request_timings, "request_start"), - start_val=request_info.scheduler_timings.targeted_start, - ) - - return state - - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[Literal["run_stats"], BenchmarkSchedulerStats]: - """ - Compile scheduler timing metrics into benchmark statistics. - - :param agg_state: Accumulated timing data and counts. - :param scheduler_state: Final scheduler execution state. - :return: Dictionary containing compiled scheduler statistics. - """ - return { - "run_stats": BenchmarkSchedulerStats( - start_time=scheduler_state.start_time, - end_time=scheduler_state.end_time, - requests_made=StatusBreakdown[int, int, int, int]( - successful=scheduler_state.successful_requests, - incomplete=scheduler_state.cancelled_requests, - errored=scheduler_state.errored_requests, - total=( - scheduler_state.successful_requests - + scheduler_state.cancelled_requests - + scheduler_state.errored_requests - ), - ), - queued_time_avg=state.get_metric( - key="queued_time", type_="avg", default=0.0 - ), - worker_resolve_start_delay_avg=state.get_metric( - key="worker_resolve_start_delay", type_="avg", default=0.0 - ), - worker_resolve_time_avg=state.get_metric( - key="worker_resolve_time", type_="avg", default=0.0 - ), - worker_resolve_end_delay_avg=state.get_metric( - key="worker_resolve_end_delay", type_="avg", default=0.0 - ), - finalized_delay_avg=state.get_metric( - key="finalized_delay", type_="avg", default=0.0 - ), - worker_targeted_start_delay_avg=state.get_metric( - key="worker_targeted_start_delay", type_="avg", default=0.0 - ), - request_start_delay_avg=state.get_metric( - key="request_start_delay", type_="avg", default=0.0 - ), - request_time_avg=state.get_metric( - key="request_time", type_="avg", default=0.0 - ), - request_targeted_start_delay_avg=state.get_metric( - key="request_targeted_start_delay", type_="avg", default=0.0 - ), - ), - } - - -@SerializableAggregator.register("generative_stats_progress") -class GenerativeStatsProgressAggregator( - SerializableAggregator[GenerationResponse, GenerationRequest] -): - """ - Tracks generative model metrics during benchmark execution. - - Aggregates token-level metrics including time to first token, inter-token - latency, and token counts for real-time progress monitoring. - """ - - @classmethod - def validated_kwargs(cls, *_args, **_kwargs) -> dict[str, Any]: - return {} - - type_: Literal["generative_stats_progress"] = Field( - default="generative_stats_progress" - ) - - def __call__( - self, - state: AggregatorState, - response: GenerationResponse | None, - request: GenerationRequest, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Aggregate generative model metrics for a completed request. - - :param agg_state: Current aggregation state to update. - :param response: Generation response with token and timing data. - :param request: The processed generation request. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Updated aggregation state for progress reporting. - """ - _ = (request,) # unused - if request_info.status not in {"completed", "errored", "cancelled"}: - # Only compile progress stats for processed requests - return None - - state["updated_generative_stats"] = True - start_time = scheduler_state.start_time - end_time = ( - safe_getattr(request_info.request_timings, "request_end") - or request_info.scheduler_timings.resolve_end - ) - duration = end_time - start_time if end_time else None - - for prefix in (request_info.status, None): - requests_count = ( - scheduler_state.processed_requests - if prefix is None - else scheduler_state.successful_requests - if request_info.status == "completed" - else scheduler_state.cancelled_requests - if request_info.status == "cancelled" - else scheduler_state.errored_requests - ) - - # Requests per Second - if duration is not None: - state.set_metric( - key="requests", - value=safe_divide(requests_count, duration), - type_="rate", - prefix=prefix, - ) - - # Request Concurrency - state.set_metric( - key="requests", - value=scheduler_state.processing_requests, - type_="avg", - prefix=prefix, - ) - - # Request Latency - state.add_metric( - key="request_latency", - value=safe_getattr(request_info.request_timings, "request_end"), - start_val=safe_getattr(request_info.request_timings, "request_start"), - prefix=prefix, - ) - - # Time to First Token - state.add_metric( - key="time_to_first_token", - value=safe_getattr(request_info.request_timings, "first_iteration"), - start_val=safe_getattr(request_info.request_timings, "request_start"), - prefix=prefix, - ) - - output_tokens = safe_getattr(response, "output_tokens") - prompt_tokens = safe_getattr(response, "prompt_tokens") - - # Inter Token Latency - state.add_metric( - key="inter_token_latency", - value=safe_getattr(request_info.request_timings, "last_iteration"), - start_val=safe_getattr(request_info.request_timings, "first_iteration"), - count=( - output_tokens - 1 if output_tokens and output_tokens > 1 else None - ), - prefix=prefix, - ) - - # Time per Output Token - state.add_metric( - key="time_per_output_token", - value=safe_getattr(request_info.request_timings, "request_start"), - start_val=safe_getattr(request_info.request_timings, "last_iteration"), - count=output_tokens, - prefix=prefix, - ) - - # Prompt Tokens - state.add_metric( - key="prompt_tokens", - value=prompt_tokens, - duration=duration, - prefix=prefix, - ) - - # Output Tokens - state.add_metric( - key="output_tokens", - value=output_tokens, - duration=duration, - prefix=prefix, - ) - - # Total Tokens - state.add_metric( - key="total_tokens", - value=( - prompt_tokens + output_tokens - if all_defined(prompt_tokens, output_tokens) - else prompt_tokens - if all_defined(prompt_tokens) - else output_tokens - if all_defined(output_tokens) - else None - ), - duration=duration, - prefix=prefix, - ) - - return state - - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[str, Any]: - """ - Compile progress metrics into final results. - - GenerativeStatsProgressAggregator is primarily for progress tracking, - so compilation returns the aggregated state as-is. - - :param agg_state: The accumulated aggregation state. - :param scheduler_state: Final scheduler execution state. - :return: The aggregated state as final results. - """ - _ = (state, scheduler_state) # unused - return {} - - -@SerializableAggregator.register("generative_requests") -class GenerativeRequestsAggregator( - SerializableAggregator[GenerationResponse, GenerationRequest], -): - """ - Compiles complete generative benchmark results with warmup/cooldown filtering. - - Aggregates request data during execution and compiles comprehensive metrics - including timing distributions, token statistics, and throughput measurements. - Supports filtering warmup and cooldown periods from final results. - """ - - @classmethod - def validated_kwargs( - cls, - request_samples: int | None = 20, - warmup: int | float | None = None, - cooldown: int | float | None = None, - **_kwargs, - ) -> dict[str, Any]: - return { - "request_samples": request_samples, - "warmup": warmup, - "cooldown": cooldown, - } - - type_: Literal["generative_requests"] = Field(default="generative_requests") - - request_samples: int | None = Field(default=20, description="") - warmup: int | float | None = Field( - default=None, - description="Number of warmup requests to ignore at benchmark start", - ) - cooldown: int | float | None = Field( - default=None, - description="Number of cooldown requests to ignore at benchmark end", - ) - _in_cooldown: bool = PrivateAttr(False) - _in_warmup: bool = PrivateAttr(False) - - def __call__( - self, - state: AggregatorState, - response: GenerationResponse | None, - request: GenerationRequest, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Collect completed requests for final compilation. - - Filters requests based on warmup/cooldown settings and categorizes by - completion status for comprehensive benchmark analysis. - - :param agg_state: Current aggregation state to update. - :param response: Generation response data. - :param request: The processed generation request. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: None, as this aggregator only collects for final compilation. - """ - # Skip invalid requests - if request_info.status not in {"completed", "canceled", "errored"} or ( - request_info.status == "canceled" - and safe_getattr(request_info.scheduler_timings, "resolve_start") is None - # Canceled requests that never started should not be kept - ): - return None - - status = { - "updated_generative_requests": True, - "requests_in_warmup": False, - "requests_in_cooldown": False, - } - - if self._is_in_warmup(request_info, scheduler_state): - status["requests_in_warmup"] = True - return status - - if self._is_in_cooldown(request_info, scheduler_state): - status["requests_in_cooldown"] = True - return status - - if "completed" not in state: - state["completed"] = [] - state["errored"] = [] - state["incomplete"] = [] - - # Categorize request by status - if request_info.status == "completed": - state["completed"].append((response, request, request_info)) - elif request_info.status == "canceled": - state["incomplete"].append((response, request, request_info)) - else: - state["errored"].append((response, request, request_info)) - - return status - - def compile( - self, - state: AggregatorState, - scheduler_state: SchedulerState, # noqa: ARG002 - ) -> dict[str, Any]: - """ - Compile aggregated requests into comprehensive benchmark results. - - Transforms collected request data into detailed metrics including timing - distributions, token statistics, throughput measurements, and status breakdowns. - - :param agg_state: Accumulated request data categorized by completion status. - :param scheduler_state: Final scheduler execution state. - :return: Complete benchmark results with metrics and request statistics. - """ - successful: list[GenerativeRequestStats] = [ - self._create_generative_request_stats(response, request, request_info) - for (response, request, request_info) in state.get("completed", []) - ] - incomplete: list[GenerativeRequestStats] = [ - self._create_generative_request_stats(response, request, request_info) - for (response, request, request_info) in state.get("incomplete", []) - ] - errored: list[GenerativeRequestStats] = [ - self._create_generative_request_stats(response, request, request_info) - for (response, request, request_info) in state.get("errored", []) - ] - - # Use all requests for metrics calculations (not sampled) - total: list[GenerativeRequestStats] = successful + incomplete + errored - total_types: list[Literal["successful", "incomplete", "error"]] = [ - *["successful"] * len(successful), - *["incomplete"] * len(incomplete), - *["error"] * len(errored), - ] - start_time = min( - [math.inf] - + [ - req.scheduler_info.request_timings.request_start - for req in total - if req.scheduler_info.request_timings.request_start is not None - ] - ) - end_time = max( - [-1 * math.inf] - + [ - req.scheduler_info.request_timings.request_end - for req in total - if req.scheduler_info.request_timings.request_end is not None - ] - ) - - return { - "start_time": start_time, - "end_time": end_time, - "request_totals": StatusBreakdown[int, int, int, int]( - successful=len(successful), - incomplete=len(incomplete), - errored=len(errored), - total=len(total), - ), - "requests": StatusBreakdown[ - list[GenerativeRequestStats], - list[GenerativeRequestStats], - list[GenerativeRequestStats], - list[GenerativeRequestStats], - ]( - successful=self._sample_request_stats(successful, self.request_samples), - incomplete=self._sample_request_stats(incomplete, self.request_samples), - errored=self._sample_request_stats(errored, self.request_samples), - ), - "metrics": GenerativeMetrics( - requests_per_second=self._calculate_requests_per_second( - statuses=total_types, requests=total - ), - request_concurrency=self._calculate_request_concurrency( - statuses=total_types, requests=total - ), - request_latency=self._calculate_request_latency( - statuses=total_types, requests=total - ), - prompt_token_count=self._calculate_prompt_token_count( - statuses=total_types, requests=total - ), - output_token_count=self._calculate_output_token_count( - statuses=total_types, requests=total - ), - total_token_count=self._calculate_total_token_count( - statuses=total_types, requests=total - ), - time_to_first_token_ms=self._calculate_time_to_first_token_ms( - statuses=total_types, requests=total - ), - time_per_output_token_ms=self._calculate_time_per_output_token_ms( - statuses=total_types, requests=total - ), - inter_token_latency_ms=self._calculate_inter_token_latency_ms( - statuses=total_types, requests=total - ), - output_tokens_per_second=self._calculate_output_tokens_per_second( - statuses=total_types, requests=total - ), - tokens_per_second=self._calculate_tokens_per_second( - statuses=total_types, requests=total - ), - ), - } - - def _is_in_warmup( - self, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> bool: - """Check if the current request is within the warmup period.""" - if self.warmup is None: - return False - - if 0 < self.warmup < 1: # Percentage-based warmup - return ( - scheduler_state.remaining_fraction is not None - and scheduler_state.remaining_fraction > (1 - self.warmup) - ) - - if self.warmup >= 1: # Count/time-based warmup - if scheduler_state.processed_requests < self.warmup: - return True - - current_time = request_info.scheduler_timings.targeted_start - return ( - current_time is not None - and (current_time - scheduler_state.start_time) < self.warmup - ) - - return False - - def _is_in_cooldown( - self, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> bool: - """Check if the current request is within the cooldown period.""" - if self.cooldown is None: - return False - - if 0 < self.cooldown < 1: # Percentage-based cooldown - return ( - scheduler_state.remaining_fraction is not None - and scheduler_state.remaining_fraction < self.cooldown - ) - - if self.cooldown >= 1: # Count/time-based cooldown - if scheduler_state.remaining_requests <= self.cooldown: - return True - - current_time = ( - request_info.scheduler_timings.resolve_end - or request_info.scheduler_timings.targeted_start - ) - return ( - current_time is not None - and scheduler_state.remaining_duration is not None - and scheduler_state.remaining_duration < self.cooldown - ) - - return False - - @classmethod - def _create_generative_request_stats( - cls, - response: GenerationResponse, - request: GenerationRequest, - request_info: ScheduledRequestInfo, - ) -> GenerativeRequestStats: - prompt_tokens = response.preferred_prompt_tokens( - settings.preferred_prompt_tokens_source - ) - output_tokens = response.preferred_output_tokens( - settings.preferred_output_tokens_source - ) - - return GenerativeRequestStats( - request_id=request.request_id, - request_type=request.request_type, - prompt=str(request.content), - request_args=response.request_args, - output=response.value, - iterations=response.iterations, - prompt_tokens=prompt_tokens, - output_tokens=output_tokens, - total_tokens=( - prompt_tokens + output_tokens - if prompt_tokens is not None and output_tokens is not None - else None - ), - scheduler_info=request_info, - ) - - @classmethod - def _sample_request_stats( - cls, stats: list[GenerativeRequestStats], sample_size: int | None - ) -> list[GenerativeRequestStats]: - if sample_size is None or sample_size <= 0 or not stats: - return stats - - return random.sample(stats, min(sample_size, len(stats))) - - @classmethod - def _calculate_requests_per_second( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_times = [] - - for status, request in zip(statuses, requests): - if not all_defined( - safe_getattr(request.scheduler_info.request_timings, "request_start"), - safe_getattr(request.scheduler_info.request_timings, "request_end"), - ): - continue - - filtered_statuses.append(status) - filtered_times.append( - ( - request.scheduler_info.request_timings.request_start, - request.scheduler_info.request_timings.request_end, - ) - ) - - return StatusDistributionSummary.from_request_times( - request_types=filtered_statuses, - requests=filtered_times, - distribution_type="rate", - ) - - @classmethod - def _calculate_request_concurrency( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_times = [] - - for status, request in zip(statuses, requests): - if not all_defined( - safe_getattr(request.scheduler_info.request_timings, "request_start"), - safe_getattr(request.scheduler_info.request_timings, "request_end"), - ): - continue - - filtered_statuses.append(status) - filtered_times.append( - ( - request.scheduler_info.request_timings.request_start, - request.scheduler_info.request_timings.request_end, - ) - ) - - return StatusDistributionSummary.from_request_times( - request_types=filtered_statuses, - requests=filtered_times, - distribution_type="concurrency", - ) - - @classmethod - def _calculate_request_latency( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.request_latency): - continue - - filtered_statuses.append(status) - filtered_values.append(request.request_latency) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_prompt_token_count( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.prompt_tokens): - continue - - filtered_statuses.append(status) - filtered_values.append(request.prompt_tokens) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_output_token_count( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.output_tokens): - continue - - filtered_statuses.append(status) - filtered_values.append(request.output_tokens) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_total_token_count( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.total_tokens): - continue - - filtered_statuses.append(status) - filtered_values.append(request.total_tokens) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_time_to_first_token_ms( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.time_to_first_token_ms): - continue - - filtered_statuses.append(status) - filtered_values.append(request.time_to_first_token_ms) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_time_per_output_token_ms( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - filtered_weights = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.time_to_first_token_ms): - continue - - # Add time to first token separately to better reflect in distribution - filtered_statuses.append(status) - filtered_values.append(request.time_to_first_token_ms) - filtered_weights.append(1) - - if not all_defined(request.inter_token_latency_ms): - continue - - # Add tokens after the first token to get the full distribution - filtered_statuses.append(status) - filtered_values.append(request.inter_token_latency_ms) - filtered_weights.append(request.output_tokens - 1) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - weights=filtered_weights, - ) - - @classmethod - def _calculate_inter_token_latency_ms( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - filtered_weights = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.inter_token_latency_ms): - continue - - filtered_statuses.append(status) - filtered_values.append(request.inter_token_latency_ms) - filtered_weights.append(request.output_tokens - 1) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - weights=filtered_weights, - ) - - @classmethod - def _calculate_output_tokens_per_second( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_request_times = [] - filtered_first_iter_times = [] - filtered_iter_counts = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.output_tokens_per_second): - continue - - filtered_statuses.append(status) - filtered_request_times.append( - ( - request.scheduler_info.request_timings.request_start, - request.scheduler_info.request_timings.request_end, - ) - ) - filtered_first_iter_times.append( - request.scheduler_info.request_timings.first_iteration - ) - filtered_iter_counts.append(request.output_tokens) - - return StatusDistributionSummary.from_iterable_request_times( - request_types=filtered_statuses, - requests=filtered_request_times, - first_iter_times=filtered_first_iter_times, - iter_counts=filtered_iter_counts, - ) - - @classmethod - def _calculate_tokens_per_second( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_request_times = [] - filtered_first_iter_times = [] - filtered_iter_counts = [] - filtered_first_iter_counts = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.tokens_per_second): - continue - - filtered_statuses.append(status) - filtered_request_times.append( - ( - request.scheduler_info.request_timings.request_start, - request.scheduler_info.request_timings.request_end, - ) - ) - filtered_first_iter_times.append( - request.scheduler_info.request_timings.first_iteration - ) - filtered_iter_counts.append(request.output_tokens - 1) - filtered_first_iter_counts.append(request.prompt_tokens + 1) - - return StatusDistributionSummary.from_iterable_request_times( - request_types=filtered_statuses, - requests=filtered_request_times, - first_iter_times=filtered_first_iter_times, - iter_counts=filtered_iter_counts, - first_iter_counts=filtered_first_iter_counts, - ) diff --git a/src/guidellm/benchmark/benchmarker.py b/src/guidellm/benchmark/benchmarker.py index 5f05065a..35b9cbf1 100644 --- a/src/guidellm/benchmark/benchmarker.py +++ b/src/guidellm/benchmark/benchmarker.py @@ -3,16 +3,9 @@ Provides the core benchmarking engine that coordinates request scheduling, data aggregation, and result compilation across different execution strategies -and environments. - -Classes: - Benchmarker: Abstract benchmark orchestrator for request processing workflows. - -Type Variables: - BenchmarkT: Generic benchmark result type. - RequestT: Generic request object type. - RequestTimingsT: Generic request timing object type. - ResponseT: Generic response object type. +and environments. The Benchmarker acts as the primary workflow coordinator, +managing the complete benchmark lifecycle from request submission through +result compilation while supporting thread-safe singleton operations. """ from __future__ import annotations @@ -20,31 +13,24 @@ import uuid from abc import ABC from collections.abc import AsyncIterator, Iterable -from typing import ( - Any, - Generic, -) +from typing import Generic -from guidellm.benchmark.aggregator import ( - Aggregator, - AggregatorState, - CompilableAggregator, -) -from guidellm.benchmark.objects import BenchmarkerDict, BenchmarkT, SchedulerDict from guidellm.benchmark.profile import Profile +from guidellm.benchmark.progress import BenchmarkerProgress +from guidellm.benchmark.schemas import ( + BenchmarkerArgs, + BenchmarkT, + EstimatedBenchmarkState, +) +from guidellm.logger import logger from guidellm.scheduler import ( BackendInterface, - Constraint, Environment, - NonDistributedEnvironment, RequestT, ResponseT, Scheduler, - SchedulerState, - SchedulingStrategy, ) -from guidellm.utils import InfoMixin, ThreadSafeSingletonMixin -from guidellm.utils.pydantic_utils import StandardBaseDict +from guidellm.utils import ThreadSafeSingletonMixin __all__ = ["Benchmarker"] @@ -57,103 +43,117 @@ class Benchmarker( """ Abstract benchmark orchestrator for request processing workflows. - Coordinates the execution of benchmarking runs across different scheduling + Coordinates execution of benchmarking runs across different scheduling strategies, aggregating metrics and compiling results. Manages the complete - benchmark lifecycle from request submission through result compilation. - - Implements thread-safe singleton pattern to ensure consistent state across - concurrent benchmark operations. + benchmark lifecycle from request submission through result compilation while + implementing thread-safe singleton pattern to ensure consistent state across + concurrent operations. """ async def run( self, + benchmark_class: type[BenchmarkT], requests: Iterable[RequestT | Iterable[RequestT | tuple[RequestT, float]]], backend: BackendInterface[RequestT, ResponseT], profile: Profile, - benchmark_class: type[BenchmarkT], - benchmark_aggregators: dict[ - str, - Aggregator[ResponseT, RequestT] | CompilableAggregator[ResponseT, RequestT], - ], - environment: Environment | None = None, - ) -> AsyncIterator[ - tuple[ - AggregatorState | None, - BenchmarkT | None, - SchedulingStrategy, - SchedulerState | None, - ] - ]: + environment: Environment, + progress: BenchmarkerProgress[BenchmarkT] | None = None, + sample_requests: int | None = 20, + warmup: float | None = None, + cooldown: float | None = None, + prefer_response_metrics: bool = True, + ) -> AsyncIterator[BenchmarkT]: """ Execute benchmark runs across multiple scheduling strategies. - Orchestrates the complete benchmark workflow: iterates through scheduling - strategies from the profile, executes requests through the scheduler, - aggregates metrics, and compiles final benchmark results. - - :param requests: Request datasets for processing across strategies. - :param backend: Backend interface for request processing. - :param profile: Benchmark profile defining strategies and constraints. - :param environment: Execution environment for coordination. - :param benchmark_aggregators: Metric aggregation functions by name. - :param benchmark_class: Class for constructing final benchmark objects. - :yield: Tuples of (metrics_update, benchmark_result, strategy, state). - :raises Exception: If benchmark execution or compilation fails. + Orchestrates the complete benchmark workflow by iterating through scheduling + strategies from the profile, executing requests through the scheduler, + aggregating metrics, and compiling final benchmark results. + + :param benchmark_class: Class for constructing final benchmark objects + :param requests: Request datasets for processing across strategies + :param backend: Backend interface for request processing + :param profile: Benchmark profile defining strategies and constraints + :param environment: Execution environment for coordination + :param progress: Optional progress tracker for benchmark lifecycle events + :param sample_requests: Number of sample requests to use for estimation + :param warmup: Optional warmup duration in seconds before benchmarking + :param cooldown: Optional cooldown duration in seconds after benchmarking + :param prefer_response_metrics: Whether to prefer response-based metrics over + request-based metrics + :yield: Compiled benchmark results for each strategy execution + :raises Exception: If benchmark execution or compilation fails """ with self.thread_lock: - if environment is None: - environment = NonDistributedEnvironment() + if progress: + await progress.on_initialize(profile) run_id = str(uuid.uuid4()) strategies_generator = profile.strategies_generator() strategy, constraints = next(strategies_generator) while strategy is not None: - yield None, None, strategy, None - aggregators_state = { - key: AggregatorState() for key in benchmark_aggregators - } + if progress: + await progress.on_benchmark_start(strategy) + + args = BenchmarkerArgs( + run_id=run_id, + run_index=len(profile.completed_strategies), + sample_requests=sample_requests, + warmup=warmup, + cooldown=cooldown, + prefer_response_metrics=prefer_response_metrics, + ) + estimated_state = EstimatedBenchmarkState() + scheduler_state = None + scheduler: Scheduler[RequestT, ResponseT] = Scheduler() async for ( response, request, request_info, scheduler_state, - ) in Scheduler[RequestT, ResponseT]().run( + ) in scheduler.run( requests=requests, backend=backend, strategy=strategy, + startup_duration=warmup if warmup and warmup >= 1 else 0.0, env=environment, **constraints or {}, ): - aggregators_update = AggregatorState() - for key, aggregator in benchmark_aggregators.items(): - update = aggregator( - aggregators_state[key], + try: + benchmark_class.update_estimate( + args, + estimated_state, response, request, request_info, scheduler_state, ) - if update: - aggregators_update.update(update) - yield aggregators_update, None, strategy, scheduler_state + if progress: + await progress.on_benchmark_update( + estimated_state, scheduler_state + ) + except Exception as err: # noqa: BLE001 + logger.error( + f"Error updating benchmark estimate/progress: {err}" + ) - benchmark_kwargs = self._compile_benchmark_kwargs( - run_id=run_id, - run_index=len(profile.completed_strategies), + benchmark = benchmark_class.compile( + args=args, + estimated_state=estimated_state, + scheduler_state=scheduler_state, profile=profile, requests=requests, backend=backend, environment=environment, - aggregators=benchmark_aggregators, - aggregators_state=aggregators_state, strategy=strategy, constraints=constraints, - scheduler_state=scheduler_state, ) - benchmark = benchmark_class(**benchmark_kwargs) - yield None, benchmark, strategy, None + if progress: + await progress.on_benchmark_complete(benchmark) + + yield benchmark try: strategy, constraints = strategies_generator.send(benchmark) @@ -161,106 +161,5 @@ async def run( strategy = None constraints = None - @classmethod - def _compile_benchmark_kwargs( - cls, - run_id: str, - run_index: int, - profile: Profile, - requests: Iterable[RequestT | Iterable[RequestT | tuple[RequestT, float]]], - backend: BackendInterface[RequestT, ResponseT], - environment: Environment, - aggregators: dict[ - str, - Aggregator[ResponseT, RequestT] | CompilableAggregator[ResponseT, RequestT], - ], - aggregators_state: dict[str, dict[str, Any]], - strategy: SchedulingStrategy, - constraints: dict[str, Any | dict[str, Any] | Constraint], - scheduler_state: SchedulerState | None, - ) -> dict[str, Any]: - """ - Compile benchmark construction parameters from execution results. - - Aggregates metadata from scheduler execution and compiles it into - structured parameters for benchmark object construction. - - :param run_id: Unique identifier for the benchmark run. - :param run_index: Index of this strategy in the benchmark profile. - :param profile: Benchmark profile containing strategy configuration. - :param requests: Request datasets used for the benchmark. - :param backend: Backend interface used for request processing. - :param environment: Execution environment for coordination. - :param aggregators: Metric aggregation functions by name. - :param aggregators_state: Current state of metric aggregators. - :param strategy: Scheduling strategy that was executed. - :param constraints: Runtime constraints applied during execution. - :param scheduler_state: Final state of scheduler execution. - :return: Dictionary of parameters for benchmark object construction. - :raises ValueError: If aggregator output conflicts with existing keys. - """ - benchmark_kwargs = { - "run_id": run_id, - "run_index": run_index, - "scheduler": SchedulerDict( - strategy=strategy, - constraints={ - key: InfoMixin.extract_from_obj(val) - for key, val in constraints.items() - }, - state=scheduler_state, - ), - "benchmarker": BenchmarkerDict( - profile=profile, - requests=InfoMixin.extract_from_obj(requests), - backend=backend.info, - environment=environment.info, - aggregators={ - key: InfoMixin.extract_from_obj(aggregator) - for key, aggregator in aggregators.items() - }, - ), - "env_args": StandardBaseDict(), - "extras": StandardBaseDict(), - } - - def _combine( - existing: dict[str, Any] | StandardBaseDict, - addition: dict[str, Any] | StandardBaseDict, - ) -> dict[str, Any] | StandardBaseDict: - if not isinstance(existing, (dict, StandardBaseDict)): - raise ValueError( - f"Existing value {existing} (type: {type(existing).__name__}) " - f"is not a valid type for merging." - ) - if not isinstance(addition, (dict, StandardBaseDict)): - raise ValueError( - f"Addition value {addition} (type: {type(addition).__name__}) " - f"is not a valid type for merging." - ) - - add_kwargs = ( - addition if isinstance(addition, dict) else addition.model_dump() - ) - - if isinstance(existing, dict): - return {**add_kwargs, **existing} - - return existing.__class__(**{**add_kwargs, **existing.model_dump()}) - - for key, aggregator in aggregators.items(): - if not isinstance(aggregator, CompilableAggregator): - continue - - compiled = aggregator.compile(aggregators_state[key], scheduler_state) - - for field_name, field_val in compiled.items(): - if field_name in benchmark_kwargs: - # If the key already exists, merge the values - benchmark_kwargs[field_name] = _combine( - benchmark_kwargs[field_name], field_val - ) - else: - benchmark_kwargs[field_name] = field_val - - return benchmark_kwargs + if progress: + await progress.on_finalize() diff --git a/src/guidellm/benchmark/entrypoints.py b/src/guidellm/benchmark/entrypoints.py index b926394f..1962f552 100644 --- a/src/guidellm/benchmark/entrypoints.py +++ b/src/guidellm/benchmark/entrypoints.py @@ -1,41 +1,49 @@ +""" +High-level entry points for executing generative text benchmarks. + +This module provides the primary interface for running generative text benchmarks +through the `benchmark_generative_text` function and re-importing existing benchmark +reports via `reimport_benchmarks_report`. It orchestrates the initialization and +coordination of backends, data loaders, profiles, and output formats to execute +comprehensive benchmarking workflows. The module handles all resolution logic for +converting user-provided arguments into fully configured components ready for +benchmarking execution. +""" + from __future__ import annotations +from collections.abc import Callable from pathlib import Path from typing import Any, Literal -from guidellm.backends import ( - Backend, - BackendType, - GenerationRequest, - GenerationResponse, -) -from guidellm.benchmark.aggregator import ( - GenerativeRequestsAggregator, - GenerativeStatsProgressAggregator, - SchedulerStatsAggregator, - SerializableAggregator, -) +from torch.utils.data import Sampler +from transformers import PreTrainedTokenizerBase +from typing_extensions import TypeAliasType + +from guidellm.backends import Backend, BackendType from guidellm.benchmark.benchmarker import Benchmarker -from guidellm.benchmark.objects import GenerativeBenchmark, GenerativeBenchmarksReport -from guidellm.benchmark.output import ( - GenerativeBenchmarkerOutput, -) +from guidellm.benchmark.output import GenerativeBenchmarkerOutput from guidellm.benchmark.profile import Profile, ProfileType -from guidellm.benchmark.progress import BenchmarkerProgressGroup -from guidellm.benchmark.scenario import enable_scenarios -from guidellm.benchmark.types import ( - AggregatorInputT, - DataInputT, - OutputFormatT, - ProcessorInputT, - ProgressInputT, +from guidellm.benchmark.progress import GenerativeConsoleBenchmarkerProgress +from guidellm.benchmark.schemas import ( + BenchmarkGenerativeTextArgs, + GenerativeBenchmark, + GenerativeBenchmarksReport, +) +from guidellm.data import ( + DataLoader, + DatasetPreprocessor, + GenerativeRequestCollator, + PreprocessorRegistry, + ProcessorFactory, ) -from guidellm.request import GenerativeRequestLoader +from guidellm.data.preprocessors import GenerativeColumnMapper from guidellm.scheduler import ( ConstraintInitializer, NonDistributedEnvironment, StrategyType, ) +from guidellm.schemas import GenerationRequest, GenerationResponse from guidellm.utils import Console, InfoMixin __all__ = [ @@ -44,271 +52,415 @@ ] -_CURRENT_WORKING_DIR = Path.cwd() +# Helper Functions +OutputFormatT = TypeAliasType( + "OutputFormatT", + tuple[str, ...] + | list[str] + | dict[str, str | dict[str, Any] | GenerativeBenchmarkerOutput] + | None, +) -# Helper functions +ProcessorInputT = TypeAliasType("ProcessorInputT", str | Path | PreTrainedTokenizerBase) -async def initialize_backend( +async def resolve_backend( backend: BackendType | Backend, target: str, model: str | None, - backend_kwargs: dict[str, Any] | None, -) -> Backend: + console: Console | None = None, + **backend_kwargs: dict[str, Any], +) -> tuple[Backend, str | None]: + """ + Initialize and validate a backend instance for benchmarking. + + :param backend: Backend type identifier or pre-configured Backend instance + :param target: Target endpoint URL or connection string for the backend + :param model: Model identifier to use with the backend, or None to use default + :param console: Console instance for progress reporting, or None + :param backend_kwargs: Additional keyword arguments passed to backend initialization + :return: Tuple of initialized Backend instance and resolved model identifier + """ + console_step = ( + console.print_update_step(title=f"Initializing backend {backend}") + if console + else None + ) backend = ( Backend.create(backend, target=target, model=model, **(backend_kwargs or {})) if not isinstance(backend, Backend) else backend ) + + if console_step: + console_step.update(f"{backend.__class__.__name__} backend initialized") + await backend.process_startup() await backend.validate() - return backend - -async def resolve_profile( - constraint_inputs: dict[str, int | float], - profile: Profile | str | None, - rate: list[float] | None, - random_seed: int, - constraints: dict[str, ConstraintInitializer | Any], -): - for key, val in constraint_inputs.items(): - if val is not None: - constraints[key] = val - if not isinstance(profile, Profile): - if isinstance(profile, str): - profile = Profile.create( - rate_type=profile, - rate=rate, - random_seed=random_seed, - constraints={**constraints}, + if model is None: + if console_step: + console_step.update( + title="Resolving default model from backend.default_model", + status_level="info", ) - else: - raise ValueError(f"Expected string for profile; got {type(profile)}") - - elif constraints: - raise ValueError( - "Constraints must be empty when providing a Profile instance. " - f"Provided constraints: {constraints} ; provided profile: {profile}" - ) - return profile - - -async def resolve_output_formats( - output_formats: OutputFormatT, - output_path: str | Path | None, -) -> dict[str, GenerativeBenchmarkerOutput]: - return GenerativeBenchmarkerOutput.resolve( - output_formats=(output_formats or {}), output_path=output_path - ) - - -async def finalize_outputs( - report: GenerativeBenchmarksReport, - resolved_output_formats: dict[str, GenerativeBenchmarkerOutput], -): - output_format_results = {} - for key, output in resolved_output_formats.items(): - output_result = await output.finalize(report) - output_format_results[key] = output_result - return output_format_results - - -# Complete entrypoints + model = await backend.default_model() + await backend.process_shutdown() -# @validate_call(config={"arbitrary_types_allowed": True}) -@enable_scenarios -async def benchmark_generative_text( # noqa: C901 - target: str, - data: DataInputT, - profile: StrategyType | ProfileType | Profile, - rate: list[float] | None = None, - random_seed: int = 42, - # Backend configuration - backend: BackendType | Backend = "openai_http", - backend_kwargs: dict[str, Any] | None = None, - model: str | None = None, - # Data configuration - processor: ProcessorInputT | None = None, - processor_args: dict[str, Any] | None = None, - data_args: dict[str, Any] | None = None, - data_sampler: Literal["random"] | None = None, - # Output configuration - output_path: str | Path | None = _CURRENT_WORKING_DIR, - output_formats: OutputFormatT = ("console", "json", "html", "csv"), - # Updates configuration - progress: ProgressInputT | None = None, - print_updates: bool = False, - # Aggregators configuration - add_aggregators: AggregatorInputT | None = None, - warmup: float | None = None, - cooldown: float | None = None, - request_samples: int | None = 20, - # Constraints configuration - max_seconds: int | float | None = None, - max_requests: int | None = None, - max_errors: int | None = None, - max_error_rate: float | None = None, - max_global_error_rate: float | None = None, - **constraints: dict[str, ConstraintInitializer | Any], -) -> tuple[GenerativeBenchmarksReport, dict[str, Any]]: - console = Console(quiet=not print_updates) - - with console.print_update_step( - title=f"Initializing backend {backend}" - ) as console_step: - backend = await initialize_backend(backend, target, model, backend_kwargs) + if console_step: console_step.finish( - title=f"{backend.__class__.__name__} backend initialized", + title=( + f"{backend.__class__.__name__} backend validated with model {model}" + ), details=backend.info, status_level="success", ) - with console.print_update_step(title="Resolving processor") as console_step: - if processor is not None: + return backend, model + + +async def resolve_processor( + processor: ProcessorInputT | None, + model: str | None, + console: Console | None = None, +) -> ProcessorInputT | None: + """ + Resolve the processor for tokenization, defaulting to model if not provided. + + :param processor: Processor identifier, path, tokenizer instance, or None + :param model: Model identifier to use as fallback processor + :param console: Console instance for progress reporting, or None + :return: Resolved processor or None if neither processor nor model provided + """ + console_step = ( + console.print_update_step(title=f"Resolving processor {processor}") + if console + else None + ) + + if processor is not None: + if console_step: console_step.finish( title="Processor resolved", details=f"Using processor '{processor}'", status_level="success", ) - elif model is not None: + else: + processor = model + if console_step: console_step.finish( title="Processor resolved", - details=f"Using model '{model}' as processor", + details=f"Using model '{processor}' as processor", status_level="success", ) - processor = model - else: - console_step.update( - title="Resolving processor from backend.default_model", - status_level="info", - ) - processor = await backend.default_model() - console_step.finish( - title="Processor resolved", - details=( - f"Using model '{processor}' from backend " - f"{backend.__class__.__name__} as processor" - ), - status_level="success", - ) - await backend.process_shutdown() - with console.print_update_step( - title=f"Initializing request loader from {data}" - ) as console_step: - request_loader = GenerativeRequestLoader( - data=data, - data_args=data_args, - processor=processor, - processor_args=processor_args, - shuffle=data_sampler == "random", - random_seed=random_seed, + return processor + + +async def resolve_request_loader( + data: list[Any], + model: str | None, + data_args: list[dict[str, Any]] | None, + data_samples: int, + processor: ProcessorInputT | None, + processor_args: dict[str, Any] | None, + data_column_mapper: ( + DatasetPreprocessor | dict[str, str] | Literal["generative_column_mapper"] + ), + data_request_formatter: (DatasetPreprocessor | dict[str, str] | str), + data_collator: Callable | Literal["generative"] | None, + data_sampler: Sampler[int] | Literal["shuffle"] | None, + data_num_workers: int | None, + random_seed: int, + console: Console | None = None, + **dataloader_kwargs: dict[str, Any] | None, +) -> DataLoader[GenerationRequest]: + """ + Construct a DataLoader for GenerationRequest objects from raw data inputs. + + :param data: List of data sources to load requests from + :param model: Model identifier for request formatting + :param data_args: Arguments for each data source in the data list + :param data_samples: Number of samples to draw from the dataset + :param processor: Processor for tokenization operations + :param processor_args: Arguments for processor initialization + :param data_column_mapper: Preprocessor or mapping for standardizing column names + :param data_request_formatter: Preprocessor or config for formatting requests + :param data_collator: Collation function or type for batching requests + :param data_sampler: Sampler instance or type for data sampling + :param data_num_workers: Number of worker processes for data loading + :param random_seed: Seed for reproducible random operations + :param console: Console instance for progress reporting, or None + :param dataloader_kwargs: Additional arguments passed to DataLoader initialization + :return: Configured DataLoader instance for GenerationRequest objects + """ + console_step = ( + console.print_update_step(title=f"Initializing request loader from {data}") + if console + else None + ) + + if not isinstance(data_column_mapper, DatasetPreprocessor): + column_mappings = ( + data_column_mapper if isinstance(data_column_mapper, dict) else None ) - unique_requests = request_loader.num_unique_items(raise_err=False) + data_column_mapper = GenerativeColumnMapper( + column_mappings=column_mappings, + ) + if not isinstance(data_request_formatter, DatasetPreprocessor): + request_type = ( + data_request_formatter + if isinstance(data_request_formatter, str) + else data_request_formatter.pop("request_type", "chat_completions") + ) + data_request_formatter = PreprocessorRegistry.get_registered_object( + request_type + )( + model=model, + **( + data_request_formatter + if isinstance(data_request_formatter, dict) + else {} + ), + ) + + request_loader = DataLoader( + data=data, + data_args=data_args, + data_samples=data_samples, + processor_factory=ProcessorFactory( + processor=processor, processor_args=processor_args + ), + preprocessors=[data_column_mapper, data_request_formatter], + collator=( + data_collator if callable(data_collator) else GenerativeRequestCollator() + ), + sampler=data_sampler, + num_workers=data_num_workers, + random_seed=random_seed, + **(dataloader_kwargs or {}), + ) + + if console_step: console_step.finish( title=( - f"Request loader initialized with {unique_requests} unique requests " - f"from {data}" + f"Request loader initialized with " + f"{data_samples if data_samples > 0 else 'inf'} " + f"unique requests from {data}" ), details=InfoMixin.extract_from_obj(request_loader), status_level="success", ) - with console.print_update_step( - title=f"Resolving profile {profile}" - ) as console_step: - profile = await resolve_profile( - { - "max_seconds": max_seconds, - "max_requests": max_requests, - "max_errors": max_errors, - "max_error_rate": max_error_rate, - "max_global_error_rate": max_global_error_rate, - }, - profile, - rate, - random_seed, - constraints, + return request_loader + + +async def resolve_profile( + profile: StrategyType | ProfileType | Profile, + rate: float | list[float] | None, + random_seed: int, + constraints: dict[str, ConstraintInitializer | Any], + max_seconds: int | float | None, + max_requests: int | None, + max_errors: int | None, + max_error_rate: float | None, + max_global_error_rate: float | None, + console: Console | None = None, +) -> Profile: + """ + Resolve and configure a benchmark profile with rate and constraint settings. + + :param profile: Profile type identifier or pre-configured Profile instance + :param rate: Request rate(s) for the benchmark execution + :param random_seed: Seed for reproducible random operations + :param constraints: Dictionary of constraint initializers for benchmark limits + :param max_seconds: Maximum duration in seconds for the benchmark + :param max_requests: Maximum number of requests to process + :param max_errors: Maximum number of errors before stopping + :param max_error_rate: Maximum error rate threshold before stopping + :param max_global_error_rate: Maximum global error rate threshold before stopping + :param console: Console instance for progress reporting, or None + :return: Configured Profile instance ready for benchmarking + :raises ValueError: If constraints are provided with a pre-configured Profile + """ + console_step = ( + console.print_update_step(title=f"Resolving profile {profile}") + if console + else None + ) + + for key, val in { + "max_seconds": max_seconds, + "max_requests": max_requests, + "max_errors": max_errors, + "max_error_rate": max_error_rate, + "max_global_error_rate": max_global_error_rate, + }.items(): + if val is not None: + constraints[key] = val + if not isinstance(profile, Profile): + profile = Profile.create( + rate_type=profile, + rate=rate, + random_seed=random_seed, + constraints={**constraints}, + ) + elif constraints: + raise ValueError( + "Constraints must be empty when providing a Profile instance. " + f"Provided constraints: {constraints} ; provided profile: {profile}" ) + + if console_step: console_step.finish( title=f"{profile.__class__.__name__} profile resolved", details=InfoMixin.extract_from_obj(profile), status_level="success", ) - with console.print_update_step( - title="Creating benchmark aggregators" - ) as console_step: - aggregators = { - "scheduler_stats": SchedulerStatsAggregator(), - "requests_progress": GenerativeStatsProgressAggregator(), - "requests": GenerativeRequestsAggregator( - request_samples=request_samples, - warmup=warmup, - cooldown=cooldown, - ), - **SerializableAggregator.resolve(add_aggregators or {}), - } - console_step.finish( - title="Benchmark aggregators created", - details={key: str(val) for key, val in aggregators.items()}, - status_level="success", - ) + return profile - with console.print_update_step(title="Resolving output formats") as console_step: - resolved_output_formats = await resolve_output_formats( - output_formats, output_path - ) + +async def resolve_output_formats( + output_formats: OutputFormatT, + output_path: str | Path | None, + console: Console | None = None, +) -> dict[str, GenerativeBenchmarkerOutput]: + """ + Resolve output format specifications into configured output handler instances. + + :param output_formats: Specification of desired output formats + :param output_path: Base path for output file generation, or None for default + :param console: Console instance for progress reporting, or None + :return: Dictionary mapping format names to configured output handler instances + """ + console_step = ( + console.print_update_step(title="Resolving output formats") if console else None + ) + + resolved = GenerativeBenchmarkerOutput.resolve( + output_formats=output_formats, output_path=output_path + ) + + if console_step: console_step.finish( title="Output formats resolved", - details={key: str(val) for key, val in resolved_output_formats.items()}, + details={key: str(val) for key, val in resolved.items()}, status_level="success", ) - progress_group = BenchmarkerProgressGroup( - instances=progress or [], enabled=bool(progress) + return resolved + + +# Main Entrypoints Functions + + +async def benchmark_generative_text( + args: BenchmarkGenerativeTextArgs, + progress: GenerativeConsoleBenchmarkerProgress | None = None, + console: Console | None = None, + **constraints: dict[str, ConstraintInitializer | Any], +) -> tuple[GenerativeBenchmarksReport, dict[str, Any]]: + """ + Execute a comprehensive generative text benchmarking workflow. + + Orchestrates the full benchmarking pipeline by resolving all components (backend, + data loader, profile, outputs) from provided arguments, executing the benchmark + runs, and finalizing results in the specified output formats. + + :param args: Configuration arguments for the benchmark execution + :param progress: Progress tracker for benchmark execution, or None for no tracking + :param console: Console instance for status reporting, or None for silent operation + :param constraints: Additional constraint initializers for benchmark limits + :return: Tuple of GenerativeBenchmarksReport and dictionary of output format results + """ + backend, model = await resolve_backend( + backend=args.backend, + target=args.target, + model=args.model, + console=console, + **(args.backend_kwargs or {}), ) - report = GenerativeBenchmarksReport() - console.print_update( - title="Setup complete, starting benchmarks...", status="success" + processor = await resolve_processor( + processor=args.processor, model=model, console=console ) - console.print("\n\n") - - async for ( - _aggregator_update, - benchmark, - _strategy, - _scheduler_state, - ) in progress_group( - profile, - Benchmarker[ - GenerativeBenchmark, - GenerationRequest, - GenerationResponse, - ]().run( - requests=request_loader, - backend=backend, - profile=profile, - environment=NonDistributedEnvironment(), - benchmark_aggregators=aggregators, - benchmark_class=GenerativeBenchmark, - ), + request_loader = await resolve_request_loader( + data=args.data, + model=model, + data_args=args.data_args, + data_samples=args.data_samples, + processor=processor, + processor_args=args.processor_args, + data_column_mapper=args.data_column_mapper, + data_request_formatter=args.data_request_formatter, + data_collator=args.data_collator, + data_sampler=args.data_sampler, + data_num_workers=args.data_num_workers, + random_seed=args.random_seed, + console=console, + **(args.dataloader_kwargs or {}), + ) + profile = await resolve_profile( + profile=args.profile, + rate=args.rate, + random_seed=args.random_seed, + constraints=constraints, + max_seconds=args.max_seconds, + max_requests=args.max_requests, + max_errors=args.max_errors, + max_error_rate=args.max_error_rate, + max_global_error_rate=args.max_global_error_rate, + console=console, + ) + output_formats = await resolve_output_formats( + output_formats=args.output_formats, + output_path=args.output_path, + console=console, + ) + + report = GenerativeBenchmarksReport(args=args) + if console: + console.print_update( + title="Setup complete, starting benchmarks...", status="success" + ) + console.print("\n\n") + + benchmarker: Benchmarker[ + GenerativeBenchmark, GenerationRequest, GenerationResponse + ] = Benchmarker() + async for benchmark in benchmarker.run( + benchmark_class=args.benchmark_cls, + requests=request_loader, + backend=backend, + profile=profile, + environment=NonDistributedEnvironment(), + progress=progress, + sample_requests=args.sample_requests, + warmup=args.warmup, + cooldown=args.cooldown, + prefer_response_metrics=args.prefer_response_metrics, ): if benchmark: report.benchmarks.append(benchmark) - output_format_results = await finalize_outputs(report, resolved_output_formats) + output_format_results = {} + for key, output in output_formats.items(): + output_result = await output.finalize(report) + output_format_results[key] = output_result - console.print("\n\n") - console.print_update( - title=f"Benchmarking complete; generated {len(report.benchmarks)} benchmark(s)", - status="success", - ) - for key, value in output_format_results.items(): - console.print_update(title=f" {key:<8}: {value}", status="debug") + if console: + console.print("\n\n") + console.print_update( + title=( + "Benchmarking complete, generated " + f"{len(report.benchmarks)} benchmark(s)" + ), + status="success", + ) + for key, value in output_format_results.items(): + console.print_update(title=f" {key:<8}: {value}", status="debug") return report, output_format_results @@ -319,13 +471,17 @@ async def reimport_benchmarks_report( output_formats: OutputFormatT = ("console", "json", "html", "csv"), ) -> tuple[GenerativeBenchmarksReport, dict[str, Any]]: """ - The command-line entry point for re-importing and displaying an - existing benchmarks report. Can also specify an output format. - Assumes the file provided exists. + Load and re-export an existing benchmarks report in specified formats. + + :param file: Path to the existing benchmark report file to load + :param output_path: Base path for output file generation, or None for default + :param output_formats: Specification of desired output formats for the report + :return: Tuple of loaded GenerativeBenchmarksReport and dictionary of output results """ console = Console() + with console.print_update_step( - title=f"Loading benchmarks from {file}" + title=f"Loading benchmarks from {file}..." ) as console_step: report = GenerativeBenchmarksReport.load_file(file) console_step.finish( @@ -333,17 +489,13 @@ async def reimport_benchmarks_report( f" loaded {len(report.benchmarks)} benchmark(s)" ) - with console.print_update_step(title="Resolving output formats") as console_step: - resolved_output_formats = await resolve_output_formats( - output_formats, output_path - ) - console_step.finish( - title="Output formats resolved", - details={key: str(val) for key, val in resolved_output_formats.items()}, - status_level="success", - ) - - output_format_results = await finalize_outputs(report, resolved_output_formats) + output_formats = await resolve_output_formats( + output_formats, output_path, console=console + ) + output_format_results = {} + for key, output in output_formats.items(): + output_result = await output.finalize(report) + output_format_results[key] = output_result for key, value in output_format_results.items(): console.print_update(title=f" {key:<8}: {value}", status="debug") diff --git a/src/guidellm/benchmark/objects.py b/src/guidellm/benchmark/objects.py deleted file mode 100644 index 8afabba9..00000000 --- a/src/guidellm/benchmark/objects.py +++ /dev/null @@ -1,473 +0,0 @@ -""" -Benchmark data models and metrics for performance measurement and analysis. - -Provides comprehensive data structures for capturing, storing, and analyzing -benchmark results from scheduler executions. Includes timing measurements, -token statistics, and performance metrics for generative AI workloads. - -Classes: - BenchmarkSchedulerStats: Scheduler timing and performance statistics. - BenchmarkMetrics: Core benchmark metrics and distributions. - BenchmarkRequestStats: Individual request processing statistics. - Benchmark: Base benchmark result container with generic metrics. - GenerativeRequestStats: Request statistics for generative AI workloads. - GenerativeMetrics: Comprehensive metrics for generative benchmarks. - GenerativeBenchmark: Complete generative benchmark results and analysis. - GenerativeBenchmarksReport: Container for multiple benchmark results. - -Type Variables: - BenchmarkMetricsT: Generic benchmark metrics type. - BenchmarkRequestStatsT: Generic request statistics type. - BenchmarkT: Generic benchmark container type. -""" - -from __future__ import annotations - -import json -import uuid -from pathlib import Path -from typing import Any, ClassVar, Generic, Literal, TypeVar - -import yaml -from pydantic import Field, computed_field - -from guidellm.benchmark.profile import ( - Profile, -) -from guidellm.scheduler import ( - ScheduledRequestInfo, - SchedulerState, - SchedulingStrategy, -) -from guidellm.utils import ( - StandardBaseDict, - StandardBaseModel, - StatusBreakdown, - StatusDistributionSummary, -) - -__all__ = [ - "Benchmark", - "BenchmarkMetrics", - "BenchmarkSchedulerStats", - "BenchmarkT", - "GenerativeBenchmark", - "GenerativeBenchmarksReport", - "GenerativeMetrics", - "GenerativeRequestStats", -] - - -class BenchmarkSchedulerStats(StandardBaseDict): - """Scheduler timing and performance statistics.""" - - start_time: float = Field( - description="Unix timestamp when the benchmark run started" - ) - end_time: float = Field(description="Unix timestamp when the benchmark run ended") - requests_made: StatusBreakdown[int, int, int, int] = Field( - description="Request counts by status: successful, incomplete, errored, total" - ) - queued_time_avg: float = Field( - description="Avg time requests spent in the queue (seconds)" - ) - worker_resolve_start_delay_avg: float = Field( - description="Avg delay before worker begins resolving req after dequeue (sec)" - ) - worker_resolve_time_avg: float = Field( - description="Avg time for worker to resolve requests (seconds)" - ) - worker_resolve_end_delay_avg: float = Field( - description="Avg delay after request end till worker resolves (seconds)" - ) - finalized_delay_avg: float = Field( - description="Avg delay after resolve til finalized with in scheduler (sec)" - ) - worker_targeted_start_delay_avg: float = Field( - description="Avg delay from targeted start to actual worker start (seconds)" - ) - request_start_delay_avg: float = Field( - description="Avg delay after resolve til request start (seconds)" - ) - request_time_avg: float = Field(description="Avg request processing time (seconds)") - request_targeted_start_delay_avg: float = Field( - description="Avg delay from targeted start to actual request start" - ) - - -class SchedulerDict(StandardBaseDict): - """Scheduler configuration and execution state dictionary.""" - - strategy: SchedulingStrategy - constraints: dict[str, dict[str, Any]] - state: SchedulerState - - -class BenchmarkerDict(StandardBaseDict): - """Benchmarker configuration and component settings dictionary.""" - - profile: Profile - requests: dict[str, Any] - backend: dict[str, Any] - environment: dict[str, Any] - aggregators: dict[str, dict[str, Any]] - - -class BenchmarkMetrics(StandardBaseDict): - """Core benchmark metrics and statistical distributions.""" - - requests_per_second: StatusDistributionSummary = Field( - description="Distribution of requests per second across benchmark execution" - ) - request_concurrency: StatusDistributionSummary = Field( - description="Distribution of concurrent request counts during execution" - ) - request_latency: StatusDistributionSummary = Field( - description="Distribution of request latencies for completed requests" - ) - - -BenchmarkMetricsT = TypeVar("BenchmarkMetricsT", bound=BenchmarkMetrics) - - -class BenchmarkRequestStats(StandardBaseDict): - """Individual request processing statistics and scheduling metadata.""" - - scheduler_info: ScheduledRequestInfo = Field( - description="Scheduler metadata and timing information for the request" - ) - - -BenchmarkRequestStatsT = TypeVar("BenchmarkRequestStatsT", bound=BenchmarkRequestStats) - - -class Benchmark(StandardBaseDict, Generic[BenchmarkMetricsT, BenchmarkRequestStatsT]): - """Base benchmark result container with execution metadata.""" - - type_: Literal["benchmark"] = "benchmark" - id_: str = Field( - default_factory=lambda: str(uuid.uuid4()), - description="Unique identifier for this benchmark execution", - ) - run_id: str = Field( - description="Identifier for the benchmarker run containing this benchmark" - ) - run_index: int = Field( - description="Sequential index of this benchmark within the benchmarker run" - ) - scheduler: SchedulerDict = Field( - description="Scheduler configuration and execution state" - ) - benchmarker: BenchmarkerDict = Field( - description="Benchmarker configuration and component settings" - ) - env_args: StandardBaseDict = Field( - description="Environment arguments and runtime configuration" - ) - extras: StandardBaseDict = Field( - description="Additional metadata and custom benchmark parameters" - ) - run_stats: BenchmarkSchedulerStats = Field( - description="Scheduler timing and performance statistics" - ) - start_time: float = Field( - default=-1.0, description="Unix timestamp when the first request was initiated" - ) - end_time: float = Field( - default=-1.0, description="Unix timestamp when the last request completed" - ) - - @computed_field # type: ignore[misc] - @property - def duration(self) -> float: - """ - Benchmark execution duration in seconds. - - :return: Time elapsed from first request start to last request completion. - """ - return self.end_time - self.start_time - - metrics: BenchmarkMetricsT = Field( - description="Performance metrics and statistical distributions" - ) - request_totals: StatusBreakdown[int, int, int, int] = Field( - description="Request counts by status: successful, incomplete, errored, total" - ) - requests: StatusBreakdown[ - list[BenchmarkRequestStatsT], - list[BenchmarkRequestStatsT], - list[BenchmarkRequestStatsT], - None, - ] = Field( - description="Request details grouped by status: successful, incomplete, errored" - ) - - -BenchmarkT = TypeVar("BenchmarkT", bound=Benchmark) - - -class GenerativeRequestStats(BenchmarkRequestStats): - """Request statistics for generative AI text generation workloads.""" - - type_: Literal["generative_request_stats"] = "generative_request_stats" - request_id: str = Field(description="Unique identifier for the request") - request_type: Literal["text_completions", "chat_completions"] = Field( - description="Type of generative request: text or chat completion" - ) - prompt: str = Field(description="Input text prompt for generation") - request_args: dict[str, Any] = Field( - description="Generation parameters and configuration options" - ) - output: str | None = Field( - description="Generated text output, if request completed successfully" - ) - iterations: int = Field( - description="Number of processing iterations for the request" - ) - prompt_tokens: int | None = Field( - description="Number of tokens in the input prompt" - ) - output_tokens: int | None = Field( - description="Number of tokens in the generated output" - ) - - @computed_field # type: ignore[misc] - @property - def total_tokens(self) -> int | None: - """ - Total token count including prompt and output tokens. - - :return: Sum of prompt and output tokens, or None if either is unavailable. - """ - if self.prompt_tokens is None and self.output_tokens is None: - return None - - return (self.prompt_tokens or 0) + (self.output_tokens or 0) - - @computed_field # type: ignore[misc] - @property - def request_latency(self) -> float | None: - """ - End-to-end request processing latency in seconds. - - :return: Duration from request start to completion, or None if unavailable. - """ - if ( - not self.scheduler_info.request_timings.request_end - or not self.scheduler_info.request_timings.request_start - ): - return None - - return ( - self.scheduler_info.request_timings.request_end - - self.scheduler_info.request_timings.request_start - ) - - @computed_field # type: ignore[misc] - @property - def time_to_first_token_ms(self) -> float | None: - """ - Time to first token generation in milliseconds. - - :return: Latency from request start to first token, or None if unavailable. - """ - if ( - not self.scheduler_info.request_timings.first_iteration - or not self.scheduler_info.request_timings.request_start - ): - return None - - return 1000 * ( - self.scheduler_info.request_timings.first_iteration - - self.scheduler_info.request_timings.request_start - ) - - @computed_field # type: ignore[misc] - @property - def time_per_output_token_ms(self) -> float | None: - """ - Average time per output token in milliseconds. - - Includes time for first token and all subsequent tokens. - - :return: Average milliseconds per output token, or None if unavailable. - """ - if ( - not self.scheduler_info.request_timings.request_start - or not self.scheduler_info.request_timings.last_iteration - or not self.output_tokens - ): - return None - - return ( - 1000 - * ( - self.scheduler_info.request_timings.last_iteration - - self.scheduler_info.request_timings.request_start - ) - / self.output_tokens - ) - - @computed_field # type: ignore[misc] - @property - def inter_token_latency_ms(self) -> float | None: - """ - Average inter-token latency in milliseconds. - - Measures time between token generations, excluding first token. - - :return: Average milliseconds between tokens, or None if unavailable. - """ - if ( - not self.scheduler_info.request_timings.first_iteration - or not self.scheduler_info.request_timings.last_iteration - or not self.output_tokens - or self.output_tokens <= 1 - ): - return None - - return ( - 1000 - * ( - self.scheduler_info.request_timings.last_iteration - - self.scheduler_info.request_timings.first_iteration - ) - / (self.output_tokens - 1) - ) - - @computed_field # type: ignore[misc] - @property - def tokens_per_second(self) -> float | None: - """ - Overall token throughput including prompt and output tokens. - - :return: Total tokens per second, or None if unavailable. - """ - if not (latency := self.request_latency) or not (tokens := self.total_tokens): - return None - - return tokens / latency - - @computed_field # type: ignore[misc] - @property - def output_tokens_per_second(self) -> float | None: - """ - Output token generation throughput. - - :return: Output tokens per second, or None if unavailable. - """ - if not (latency := self.request_latency) or not self.output_tokens: - return None - - return self.output_tokens / latency - - -class GenerativeMetrics(BenchmarkMetrics): - """Comprehensive metrics for generative AI benchmarks.""" - - prompt_token_count: StatusDistributionSummary = Field( - description="Distribution of prompt token counts by request status" - ) - output_token_count: StatusDistributionSummary = Field( - description="Distribution of output token counts by request status" - ) - total_token_count: StatusDistributionSummary = Field( - description="Distribution of total token counts by request status" - ) - time_to_first_token_ms: StatusDistributionSummary = Field( - description="Distribution of first token latencies in milliseconds" - ) - time_per_output_token_ms: StatusDistributionSummary = Field( - description="Distribution of average time per output token in milliseconds" - ) - inter_token_latency_ms: StatusDistributionSummary = Field( - description="Distribution of inter-token latencies in milliseconds" - ) - output_tokens_per_second: StatusDistributionSummary = Field( - description="Distribution of output token generation rates" - ) - tokens_per_second: StatusDistributionSummary = Field( - description="Distribution of total token throughput including prompt and output" - ) - - -class GenerativeBenchmark(Benchmark[GenerativeMetrics, GenerativeRequestStats]): - """Complete generative AI benchmark results with specialized metrics.""" - - type_: Literal["generative_benchmark"] = "generative_benchmark" # type: ignore[assignment] - - -class GenerativeBenchmarksReport(StandardBaseModel): - """Container for multiple benchmark results with load/save functionality.""" - - DEFAULT_FILE: ClassVar[str] = "benchmarks.json" - - @staticmethod - def load_file( - path: str | Path, type_: Literal["json", "yaml"] | None = None - ) -> GenerativeBenchmarksReport: - """ - Load a report from a file. - - :param path: The path to load the report from. - :param type_: File type override, auto-detected from extension if None. - :return: The loaded report. - :raises ValueError: If file type is unsupported. - """ - path = Path(path) if not isinstance(path, Path) else path - - if path.is_dir(): - path = path / GenerativeBenchmarksReport.DEFAULT_FILE - - path.parent.mkdir(parents=True, exist_ok=True) - path_suffix = path.suffix.lower()[1:] - - with path.open("r") as file: - if (type_ or path_suffix) == "json": - model_dict = json.loads(file.read()) - elif (type_ or path_suffix) in ["yaml", "yml"]: - model_dict = yaml.safe_load(file) - else: - raise ValueError(f"Unsupported file type: {type_} for {path}.") - - return GenerativeBenchmarksReport.model_validate(model_dict) - - benchmarks: list[GenerativeBenchmark] = Field( - description="The list of completed benchmarks contained within the report.", - default_factory=list, - ) - - def save_file( - self, path: str | Path | None, type_: Literal["json", "yaml"] | None = None - ) -> Path: - """ - Save the report to a file. - - :param path: The path to save the report to. - :param type_: File type override, auto-detected from extension if None. - :return: The path to the saved report. - :raises ValueError: If file type is unsupported. - """ - if path is None: - path = Path.cwd() - elif not isinstance(path, Path): - path = Path(path) - - if path.is_dir(): - path = path / GenerativeBenchmarksReport.DEFAULT_FILE - - path.parent.mkdir(parents=True, exist_ok=True) - path_suffix = path.suffix.lower()[1:] - model_dict = self.model_dump() - - if (type_ or path_suffix) == "json": - save_str = json.dumps(model_dict) - elif (type_ or path_suffix) in ["yaml", "yml"]: - save_str = yaml.dump(model_dict) - else: - raise ValueError(f"Unsupported file type: {type_} for {path}.") - - with path.open("w") as file: - file.write(save_str) - - return path diff --git a/src/guidellm/benchmark/output.py b/src/guidellm/benchmark/output.py index 56775dac..1e92c7a9 100644 --- a/src/guidellm/benchmark/output.py +++ b/src/guidellm/benchmark/output.py @@ -15,17 +15,17 @@ from rich.padding import Padding from rich.text import Text -from guidellm.benchmark.objects import ( - GenerativeBenchmark, - GenerativeBenchmarksReport, - GenerativeMetrics, -) from guidellm.benchmark.profile import ( AsyncProfile, ConcurrentProfile, SweepProfile, ThroughputProfile, ) +from guidellm.benchmark.schemas import ( + GenerativeBenchmark, + GenerativeBenchmarksReport, + GenerativeMetrics, +) from guidellm.presentation import UIDataBuilder from guidellm.presentation.injector import create_report from guidellm.settings import settings @@ -34,10 +34,11 @@ DistributionSummary, RegistryMixin, StatusDistributionSummary, + camelize_str, + recursive_key_update, safe_format_timestamp, split_text_list_by_length, ) -from guidellm.utils import recursive_key_update, camelize_str __all__ = [ "GenerativeBenchmarkerCSV", @@ -90,7 +91,7 @@ def resolve( if not output_formats: return {} - if isinstance(output_formats, (list, tuple)): + if isinstance(output_formats, list | tuple): # support list of output keys: ["csv", "json"] # support list of files: ["path/to/file.json", "path/to/file.csv"] formats_list = output_formats @@ -369,7 +370,7 @@ def _print_line( f"Value and style length mismatch: {len(value)} vs {len(style)}" ) - for val, sty in zip(value, style): + for val, sty in zip(value, style, strict=False): text.append(val, style=sty) self.console.print(Padding.indent(text, indent)) @@ -568,8 +569,8 @@ async def finalize(self, report: GenerativeBenchmarksReport) -> Path: benchmark_values: list[str | float | list[float]] = [] # Add basic run description info - desc_headers, desc_values = ( - self._get_benchmark_desc_headers_and_values(benchmark) + desc_headers, desc_values = self._get_benchmark_desc_headers_and_values( + benchmark ) benchmark_headers.extend(desc_headers) benchmark_values.extend(desc_values) @@ -680,7 +681,8 @@ def _get_benchmark_status_metrics_stats( return headers, values def _get_benchmark_extras_headers_and_values( - self, benchmark: GenerativeBenchmark, + self, + benchmark: GenerativeBenchmark, ) -> tuple[list[str], list[str]]: headers = ["Profile", "Backend", "Generator Data"] values: list[str] = [ @@ -733,9 +735,7 @@ async def finalize(self, report: GenerativeBenchmarksReport) -> Path: ui_api_data = {} for k, v in camel_data.items(): placeholder_key = f"window.{k} = {{}};" - replacement_value = ( - f"window.{k} = {json.dumps(v, indent=2)};\n" - ) + replacement_value = f"window.{k} = {json.dumps(v, indent=2)};\n" ui_api_data[placeholder_key] = replacement_value create_report(ui_api_data, output_path) diff --git a/src/guidellm/benchmark/profile.py b/src/guidellm/benchmark/profile.py index 3ff8d0e0..4b3f36fd 100644 --- a/src/guidellm/benchmark/profile.py +++ b/src/guidellm/benchmark/profile.py @@ -1,32 +1,17 @@ """ -Benchmarking profile configurations for coordinating multi-strategy execution. - -Provides configurable profile abstractions for orchestrating sequential and -parallel execution of different scheduling strategies during benchmarking, -with automatic strategy generation and constraint management. - -Classes: - Profile: Abstract base for multi-strategy benchmarking profiles. - SynchronousProfile: Single synchronous strategy execution profile. - ConcurrentProfile: Fixed-concurrency strategy execution profile. - ThroughputProfile: Maximum throughput strategy execution profile. - AsyncProfile: Rate-based asynchronous strategy execution profile. - SweepProfile: Adaptive multi-strategy sweep execution profile. - -Type Aliases: - ProfileType: Literal type for supported profile configurations. +Profile configurations for orchestrating multi-strategy benchmark execution. + +Provides configurable abstractions for coordinating sequential execution of +scheduling strategies during benchmarking workflows. Profiles automatically +generate strategies based on configuration parameters, manage runtime +constraints, and track completion state across the execution sequence. """ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Generator -from typing import ( - TYPE_CHECKING, - Any, - ClassVar, - Literal, -) +from typing import TYPE_CHECKING, Any, ClassVar, Literal import numpy as np from pydantic import ( @@ -55,7 +40,7 @@ from guidellm.utils import PydanticClassRegistryMixin if TYPE_CHECKING: - from guidellm.benchmark.objects import Benchmark + from guidellm.benchmark.schemas import Benchmark __all__ = [ "AsyncProfile", @@ -75,11 +60,14 @@ class Profile( ABC, ): """ - Abstract base for multi-strategy benchmarking execution profiles. + Abstract base for coordinating multi-strategy benchmark execution. + + Manages sequential execution of scheduling strategies with automatic strategy + generation, constraint management, and completion tracking. Subclasses define + specific execution patterns like synchronous, concurrent, throughput-focused, + rate-based async, or adaptive sweep profiles. - Coordinates sequential execution of scheduling strategies with automatic - strategy generation, constraint management, and completion tracking for - comprehensive benchmarking workflows. + :cvar schema_discriminator: Field name used for polymorphic deserialization """ schema_discriminator: ClassVar[str] = "type_" @@ -100,14 +88,14 @@ def create( **kwargs: Any, ) -> Profile: """ - Create a profile instance based on the specified type. + Factory method to create a profile instance based on type. - :param rate_type: The type of profile to create. - :param rate: Rate parameter for profile configuration. - :param random_seed: Random seed for stochastic strategies. - :param kwargs: Additional arguments for profile configuration. - :return: Configured profile instance for the specified type. - :raises ValueError: If the profile type is not registered. + :param rate_type: Profile type identifier to instantiate + :param rate: Rate configuration for the profile strategy + :param random_seed: Seed for stochastic strategy reproducibility + :param kwargs: Additional profile-specific configuration parameters + :return: Configured profile instance for the specified type + :raises ValueError: If rate_type is not registered """ profile_class: type[Profile] = cls.get_registered_object(rate_type) resolved_kwargs = profile_class.resolve_args( @@ -128,33 +116,31 @@ def resolve_args( """ Resolve and validate arguments for profile construction. - :param rate_type: The type of the profile. - :param rate: Rate parameter for configuration. - :param random_seed: Random seed for stochastic strategies. - :param kwargs: Additional arguments to resolve. - :return: Dictionary of resolved arguments for profile construction. + :param rate_type: Profile type identifier + :param rate: Rate configuration parameter + :param random_seed: Seed for stochastic strategies + :param kwargs: Additional arguments to resolve and validate + :return: Resolved arguments dictionary for profile initialization """ ... type_: Literal["profile"] = Field( - description="The type of benchmarking profile to use", + description="Profile type discriminator for polymorphic serialization", ) completed_strategies: list[SchedulingStrategy] = Field( default_factory=list, - description="The strategies that have completed execution", + description="Strategies that have completed execution in this profile", ) constraints: dict[str, Any | dict[str, Any] | ConstraintInitializer] | None = Field( default=None, - description="Runtime constraints to apply during strategy execution", + description="Runtime constraints applied to strategy execution", ) @computed_field # type: ignore[misc] @property def strategy_types(self) -> list[StrategyType]: """ - :return: List of all strategy types expected to be executed or have been - executed in this profile. By default, this returns just the - completed strategies. + :return: Strategy types executed or expected to execute in this profile """ return [strat.type_ for strat in self.completed_strategies] @@ -169,10 +155,10 @@ def strategies_generator( None, ]: """ - Generate strategies and constraints for sequential profile execution. + Generate strategies and constraints for sequential execution. - :return: Generator yielding (strategy, constraints) tuples and - receiving benchmark results from each execution. + :return: Generator yielding (strategy, constraints) tuples and receiving + benchmark results after each execution """ prev_strategy: SchedulingStrategy | None = None prev_benchmark: Benchmark | None = None @@ -197,11 +183,11 @@ def next_strategy( prev_benchmark: Benchmark | None, ) -> SchedulingStrategy | None: """ - Generate the next strategy to execute in the profile sequence. + Generate the next strategy in the profile execution sequence. - :param prev_strategy: The previously completed strategy. - :param prev_benchmark: Benchmark results from the previous strategy. - :return: Next strategy to execute, or None if profile is complete. + :param prev_strategy: Previously completed strategy instance + :param prev_benchmark: Benchmark results from previous strategy execution + :return: Next strategy to execute, or None if profile complete """ ... @@ -214,10 +200,10 @@ def next_strategy_constraints( """ Generate constraints for the next strategy execution. - :param next_strategy: The next strategy to be executed. - :param prev_strategy: The previously completed strategy. - :param prev_benchmark: Benchmark results from the previous strategy. - :return: Constraints dictionary for the next strategy, or None. + :param next_strategy: Strategy to be executed next + :param prev_strategy: Previously completed strategy instance + :param prev_benchmark: Benchmark results from previous strategy execution + :return: Constraints dictionary for next strategy, or None """ _ = (prev_strategy, prev_benchmark) # unused return ( @@ -281,12 +267,12 @@ def resolve_args( """ Resolve arguments for synchronous profile construction. - :param rate_type: The type/strategy of the profile (ignored). - :param rate: Rate parameter (must be None, will be stripped). - :param random_seed: Random seed (ignored and stripped). - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. - :raises ValueError: If rate is not None. + :param rate_type: Profile type identifier (ignored) + :param rate: Rate parameter (must be None) + :param random_seed: Random seed (ignored) + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary + :raises ValueError: If rate is not None """ _ = (rate_type, random_seed) # unused if rate is not None: @@ -297,7 +283,7 @@ def resolve_args( @property def strategy_types(self) -> list[StrategyType]: """ - :return: The single synchronous strategy type. + :return: Single synchronous strategy type """ return [self.type_] @@ -309,9 +295,9 @@ def next_strategy( """ Generate synchronous strategy or None if already completed. - :param prev_strategy: The previously completed strategy (unused). - :param prev_benchmark: Benchmark results from the previous strategy (unused). - :return: SynchronousStrategy for the first execution, None afterward. + :param prev_strategy: Previously completed strategy (unused) + :param prev_benchmark: Benchmark results from previous execution (unused) + :return: SynchronousStrategy for first execution, None afterward """ _ = (prev_strategy, prev_benchmark) # unused if len(self.completed_strategies) >= 1: @@ -326,7 +312,7 @@ class ConcurrentProfile(Profile): type_: Literal["concurrent"] = "concurrent" # type: ignore[assignment] streams: list[PositiveInt] = Field( - description="Number of concurrent streams for request scheduling", + description="Concurrent stream counts for request scheduling", ) startup_duration: NonNegativeFloat = Field( default=0.0, @@ -347,20 +333,23 @@ def resolve_args( """ Resolve arguments for concurrent profile construction. - :param rate_type: The type/strategy of the profile (ignored). - :param rate: Rate parameter, remapped to streams. - :param random_seed: Random seed (ignored and stripped). - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. - :raises ValueError: If rate is None. + :param rate_type: Profile type identifier (ignored) + :param rate: Rate parameter remapped to streams + :param random_seed: Random seed (ignored) + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary + :raises ValueError: If rate is None """ _ = (rate_type, random_seed) # unused - kwargs["streams"] = [int(r) for r in rate] if rate else None + rate = rate if isinstance(rate, list) or rate is None else [rate] + kwargs["streams"] = [int(stream) for stream in rate] if rate else None return kwargs @property def strategy_types(self) -> list[StrategyType]: - """Get concurrent strategy types for each configured stream count.""" + """ + :return: Concurrent strategy types for each configured stream count + """ return [self.type_] * len(self.streams) def next_strategy( @@ -371,9 +360,9 @@ def next_strategy( """ Generate concurrent strategy for the next stream count. - :param prev_strategy: The previously completed strategy (unused). - :param prev_benchmark: Benchmark results from the previous strategy (unused). - :return: ConcurrentStrategy with next stream count, or None if complete. + :param prev_strategy: Previously completed strategy (unused) + :param prev_benchmark: Benchmark results from previous execution (unused) + :return: ConcurrentStrategy with next stream count, or None if complete """ _ = (prev_strategy, prev_benchmark) # unused @@ -395,7 +384,7 @@ class ThroughputProfile(Profile): type_: Literal["throughput"] = "throughput" # type: ignore[assignment] max_concurrency: PositiveInt | None = Field( default=None, - description="Maximum number of concurrent requests to schedule", + description="Maximum concurrent requests to schedule", ) startup_duration: NonNegativeFloat = Field( default=0.0, @@ -416,11 +405,11 @@ def resolve_args( """ Resolve arguments for throughput profile construction. - :param rate_type: The type/strategy of the profile (ignored). - :param rate: Rate parameter to remap to max_concurrency. - :param random_seed: Random seed (ignored and stripped). - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. + :param rate_type: Profile type identifier (ignored) + :param rate: Rate parameter remapped to max_concurrency + :param random_seed: Random seed (ignored) + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary """ _ = (rate_type, random_seed) # unused # Remap rate to max_concurrency, strip out random_seed @@ -431,7 +420,9 @@ def resolve_args( @property def strategy_types(self) -> list[StrategyType]: - """Get the single throughput strategy type.""" + """ + :return: Single throughput strategy type + """ return [self.type_] def next_strategy( @@ -442,9 +433,9 @@ def next_strategy( """ Generate throughput strategy or None if already completed. - :param prev_strategy: The previously completed strategy (unused). - :param prev_benchmark: Benchmark results from the previous strategy (unused). - :return: ThroughputStrategy for the first execution, None afterward. + :param prev_strategy: Previously completed strategy (unused) + :param prev_benchmark: Benchmark results from previous execution (unused) + :return: ThroughputStrategy for first execution, None afterward """ _ = (prev_strategy, prev_benchmark) # unused if len(self.completed_strategies) >= 1: @@ -458,13 +449,11 @@ def next_strategy( @Profile.register(["async", "constant", "poisson"]) class AsyncProfile(Profile): - """ - Rate-based asynchronous strategy execution profile with configurable patterns. - """ + """Rate-based asynchronous strategy execution profile with configurable patterns.""" type_: Literal["async", "constant", "poisson"] = "async" # type: ignore[assignment] strategy_type: Literal["constant", "poisson"] = Field( - description="Type of asynchronous strategy pattern to use", + description="Asynchronous strategy pattern type to use", ) rate: list[PositiveFloat] = Field( description="Request scheduling rate in requests per second", @@ -478,7 +467,7 @@ class AsyncProfile(Profile): ) max_concurrency: PositiveInt | None = Field( default=None, - description="Maximum number of concurrent requests to schedule", + description="Maximum concurrent requests to schedule", ) random_seed: int = Field( default=42, @@ -496,12 +485,12 @@ def resolve_args( """ Resolve arguments for async profile construction. - :param rate_type: The type/strategy of the profile. - :param rate: Rate parameter for the profile. - :param random_seed: Random seed for stochastic strategies. - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. - :raises ValueError: If rate is None. + :param rate_type: Profile type identifier + :param rate: Rate configuration for the profile + :param random_seed: Seed for stochastic strategies + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary + :raises ValueError: If rate is None """ if rate is None: raise ValueError("AsyncProfile requires a rate parameter") @@ -516,13 +505,15 @@ def resolve_args( if rate_type in ["constant", "poisson"] else kwargs.get("strategy_type", "constant") ) - kwargs["rate"] = rate + kwargs["rate"] = rate if isinstance(rate, list) else [rate] kwargs["random_seed"] = random_seed return kwargs @property def strategy_types(self) -> list[StrategyType]: - """Get async strategy types for each configured rate.""" + """ + :return: Async strategy types for each configured rate + """ num_strategies = len(self.rate) return [self.strategy_type] * num_strategies @@ -534,11 +525,11 @@ def next_strategy( """ Generate async strategy for the next configured rate. - :param prev_strategy: The previously completed strategy (unused). - :param prev_benchmark: Benchmark results from the previous strategy (unused). + :param prev_strategy: Previously completed strategy (unused) + :param prev_benchmark: Benchmark results from previous execution (unused) :return: AsyncConstantStrategy or AsyncPoissonStrategy for next rate, - or None if all rates completed. - :raises ValueError: If strategy_type is neither 'constant' nor 'poisson'. + or None if all rates completed + :raises ValueError: If strategy_type is neither 'constant' nor 'poisson' """ _ = (prev_strategy, prev_benchmark) # unused @@ -566,9 +557,7 @@ def next_strategy( @Profile.register("sweep") class SweepProfile(Profile): - """ - Adaptive multi-strategy sweep execution profile with rate discovery. - """ + """Adaptive multi-strategy sweep execution profile with rate discovery.""" type_: Literal["sweep"] = "sweep" # type: ignore[assignment] sweep_size: int = Field( @@ -585,7 +574,7 @@ class SweepProfile(Profile): ) max_concurrency: PositiveInt | None = Field( default=None, - description="Maximum number of concurrent requests to schedule", + description="Maximum concurrent requests to schedule", ) random_seed: int = Field( default=42, @@ -605,7 +594,7 @@ class SweepProfile(Profile): ) measured_rates: list[float] = Field( default_factory=list, - description="Calculated interpolated rates between synchronous and throughput", + description="Interpolated rates between synchronous and throughput", ) @classmethod @@ -619,11 +608,11 @@ def resolve_args( """ Resolve arguments for sweep profile construction. - :param rate_type: The type/strategy for async strategies in the sweep. - :param rate: Rate parameter (ignored for sweep). - :param random_seed: Random seed for stochastic strategies. - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. + :param rate_type: Async strategy type for sweep execution + :param rate: Rate parameter specifying sweep size (if provided) + :param random_seed: Seed for stochastic strategies + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary """ sweep_size_from_rate = int(rate[0]) if rate else settings.default_sweep_number kwargs["sweep_size"] = kwargs.get("sweep_size", sweep_size_from_rate) @@ -634,7 +623,9 @@ def resolve_args( @property def strategy_types(self) -> list[StrategyType]: - """Get strategy types for the complete sweep sequence.""" + """ + :return: Strategy types for the complete sweep sequence + """ types = ["synchronous", "throughput"] types += [self.strategy_type] * (self.sweep_size - len(types)) return types @@ -653,21 +644,21 @@ def next_strategy( """ Generate the next strategy in the adaptive sweep sequence. - Executes synchronous and throughput strategies first to measure - baseline rates, then generates interpolated rates for async strategies. + Executes synchronous and throughput strategies first to measure baseline + rates, then generates interpolated rates for async strategies. - :param prev_strategy: The previously completed strategy. - :param prev_benchmark: Benchmark results from the previous strategy. - :return: Next strategy in sweep sequence, or None if complete. - :raises ValueError: If strategy_type is neither 'constant' nor 'poisson'. + :param prev_strategy: Previously completed strategy instance + :param prev_benchmark: Benchmark results from previous strategy execution + :return: Next strategy in sweep sequence, or None if complete + :raises ValueError: If strategy_type is neither 'constant' nor 'poisson' """ if prev_strategy is None: return SynchronousStrategy() if prev_strategy.type_ == "synchronous": - self.synchronous_rate = ( - prev_benchmark.metrics.requests_per_second.successful.mean - ) + self.synchronous_rate = prev_benchmark.get_request_metrics_sample()[ + "request_throughput" + ] return ThroughputStrategy( max_concurrency=self.max_concurrency, @@ -675,11 +666,14 @@ def next_strategy( ) if prev_strategy.type_ == "throughput": - self.throughput_rate = ( - prev_benchmark.metrics.requests_per_second.successful.mean - ) + self.throughput_rate = prev_benchmark.get_request_metrics_sample()[ + "request_throughput" + ] if self.synchronous_rate <= 0 and self.throughput_rate <= 0: - raise RuntimeError("Invalid rates in sweep; aborting. Were there any successful requests?") + raise RuntimeError( + "Invalid rates in sweep; aborting. " + "Were there any successful requests?" + ) self.measured_rates = list( np.linspace( self.synchronous_rate, diff --git a/src/guidellm/benchmark/progress.py b/src/guidellm/benchmark/progress.py index f93b3a83..558def67 100644 --- a/src/guidellm/benchmark/progress.py +++ b/src/guidellm/benchmark/progress.py @@ -16,9 +16,7 @@ from __future__ import annotations -import asyncio from abc import ABC, abstractmethod -from collections.abc import AsyncIterable, AsyncIterator, Iterable from dataclasses import dataclass from datetime import datetime from typing import Any, Generic, Literal @@ -37,21 +35,16 @@ TimeRemainingColumn, ) -from guidellm.benchmark.aggregator import AggregatorState -from guidellm.benchmark.objects import BenchmarkT, GenerativeBenchmark from guidellm.benchmark.profile import Profile -from guidellm.scheduler import ( - SchedulerState, - SchedulingStrategy, - StrategyType, +from guidellm.benchmark.schemas import ( + BenchmarkT, + EstimatedBenchmarkState, + GenerativeBenchmark, ) +from guidellm.scheduler import SchedulerState, SchedulingStrategy, StrategyType from guidellm.utils import Colors, format_value_display -__all__ = [ - "BenchmarkerProgress", - "BenchmarkerProgressGroup", - "GenerativeConsoleBenchmarkerProgress", -] +__all__ = ["BenchmarkerProgress", "GenerativeConsoleBenchmarkerProgress"] class BenchmarkerProgress(Generic[BenchmarkT], ABC): @@ -63,106 +56,15 @@ class BenchmarkerProgress(Generic[BenchmarkT], ABC): enable/disable functionality for conditional progress tracking. """ - def __init__(self, enabled: bool = True): + def __init__(self): """ Initialize progress tracker. :param enabled: Whether to enable progress tracking and display. """ - self._enabled = enabled self.profile: Profile = None self.current_strategy: SchedulingStrategy = None - @property - def enabled(self) -> bool: - """ - :return: Whether progress tracking is currently enabled. - """ - return self._enabled - - @enabled.setter - def enabled(self, value: bool) -> None: - """ - :param value: True to enable progress tracking, False to disable. - :raises RuntimeError: If called after progress run has started. - """ - if self.profile is not None: - raise RuntimeError( - "Cannot change enabled state after __call__ for progress run" - ) - - self._enabled = value - - def __call__( - self, - profile: Profile, - agen: AsyncIterable[ - tuple[ - AggregatorState | None, - BenchmarkT | None, - SchedulingStrategy, - SchedulerState | None, - ] - ], - ) -> AsyncIterator[ - tuple[ - AggregatorState | None, - BenchmarkT | None, - SchedulingStrategy, - SchedulerState | None, - ] - ]: - """ - Track progress through benchmark execution pipeline. - - Wraps the provided async generator to monitor benchmark progress, - calling appropriate lifecycle hooks based on execution state. - - :param profile: Benchmark profile configuration. - :param agen: Async generator yielding benchmark execution updates. - :return: Async iterator forwarding original updates with progress tracking. - """ - - async def aiterator() -> AsyncIterator[ - tuple[ - AggregatorState | None, - BenchmarkT | None, - SchedulingStrategy, - SchedulerState | None, - ] - ]: - self.profile = profile - if self.enabled: - await self.on_initialize(profile) - - async for aggregator_update, benchmark, strategy, scheduler_state in agen: - if self.enabled: - await self.on_raw_update( - profile, - aggregator_update, - benchmark, - strategy, - scheduler_state, - ) - - if self.current_strategy != strategy: - self.current_strategy = strategy - await self.on_benchmark_start(strategy) - elif benchmark is not None: - await self.on_benchmark_complete(benchmark) - self.current_strategy = None - else: - await self.on_benchmark_update( - aggregator_update, scheduler_state - ) - - yield aggregator_update, benchmark, strategy, scheduler_state - - if self.enabled: - await self.on_finalize() - - return aiterator() - @abstractmethod async def on_initialize(self, profile: Profile): """ @@ -181,12 +83,12 @@ async def on_benchmark_start(self, strategy: SchedulingStrategy): @abstractmethod async def on_benchmark_update( - self, aggregator_update: AggregatorState, scheduler_state: SchedulerState + self, estimated_state: EstimatedBenchmarkState, scheduler_state: SchedulerState ): """ Handle benchmark execution progress update. - :param aggregator_update: Current benchmark metrics and statistics. + :param estimated_state: Current benchmark metrics and statistics. :param scheduler_state: Current scheduler execution state. """ @@ -202,153 +104,6 @@ async def on_benchmark_complete(self, benchmark: BenchmarkT): async def on_finalize(self): """Finalize progress tracking and cleanup resources.""" - async def on_raw_update( - self, - profile: Profile, - aggregator_update: AggregatorState | None, - benchmark: BenchmarkT | None, - strategy: SchedulingStrategy, - scheduler_state: SchedulerState | None, - ): - """ - Handle raw benchmark execution update. - - Optional hook for accessing all execution state updates. Default - implementation does nothing. - - :param profile: Benchmark profile configuration. - :param aggregator_update: Current benchmark metrics and statistics. - :param benchmark: Completed benchmark if available. - :param strategy: Current scheduling strategy. - :param scheduler_state: Current scheduler execution state. - """ - - -class BenchmarkerProgressGroup(BenchmarkerProgress[BenchmarkT]): - """ - Composite progress handler that manages multiple progress instances. - - Distributes progress events to all contained progress instances, enabling - parallel progress tracking through multiple channels (e.g., console display - and file logging). - - :param instances: Collection of progress handlers to manage. - :param enabled: Whether the group is active. - """ - - def __init__( - self, - instances: ( - Iterable[BenchmarkerProgress[BenchmarkT]] - | list[BenchmarkerProgress[BenchmarkT]] - ), - enabled: bool = True, - ): - """ - Initialize progress group with handler instances. - - :param instances: Progress handler instances to coordinate. - :param enabled: Whether to enable the progress group. - """ - self.instances: list[BenchmarkerProgress[BenchmarkT]] = list(instances) - super().__init__(enabled=enabled) - - @property - def enabled(self) -> bool: - """Whether the progress group is currently enabled.""" - return self._enabled - - @enabled.setter - def enabled(self, value: bool): - """ - Set enabled state for group and all contained instances. - - :param value: New enabled state. - """ - self._enabled = value - for instance in self.instances: - instance.enabled = value - - async def on_initialize(self, profile: Profile): - """ - Initialize all progress handler instances. - - :param profile: Benchmark profile configuration. - """ - await asyncio.gather( - *[child.on_initialize(profile) for child in self.instances] - ) - - async def on_benchmark_start(self, strategy: SchedulingStrategy): - """ - Notify all handlers of benchmark strategy start. - - :param strategy: Scheduling strategy being executed. - """ - await asyncio.gather( - *[child.on_benchmark_start(strategy) for child in self.instances] - ) - - async def on_benchmark_update( - self, aggregator_update: AggregatorState, scheduler_state: SchedulerState - ): - """ - Distribute benchmark updates to all handlers. - - :param aggregator_update: Current benchmark metrics and statistics. - :param scheduler_state: Current scheduler execution state. - """ - await asyncio.gather( - *[ - child.on_benchmark_update(aggregator_update, scheduler_state) - for child in self.instances - ] - ) - - async def on_benchmark_complete(self, benchmark: BenchmarkT): - """ - Notify all handlers of benchmark completion. - - :param benchmark: Completed benchmark results. - """ - await asyncio.gather( - *[child.on_benchmark_complete(benchmark) for child in self.instances] - ) - - async def on_finalize(self): - """Finalize all progress handler instances.""" - await asyncio.gather(*[child.on_finalize() for child in self.instances]) - - async def on_raw_update( - self, - profile: Profile, - aggregator_update: AggregatorState | None, - benchmark: BenchmarkT | None, - strategy: SchedulingStrategy, - scheduler_state: SchedulerState | None, - ): - """ - Distribute raw updates to all handlers. - - :param profile: Benchmark profile configuration. - :param aggregator_update: Current benchmark metrics and statistics. - :param benchmark: Completed benchmark if available. - :param strategy: Current scheduling strategy. - :param scheduler_state: Current scheduler execution state. - """ - await asyncio.gather( - *[ - child.on_raw_update( - profile, - aggregator_update, - benchmark, - strategy, - scheduler_state, - ) - for child in self.instances - ] - ) - class GenerativeConsoleBenchmarkerProgress( BenchmarkerProgress[GenerativeBenchmark], Live @@ -361,14 +116,14 @@ class GenerativeConsoleBenchmarkerProgress( bars in a structured console interface. """ - def __init__(self, enabled: bool = True, display_scheduler_stats: bool = False): + def __init__(self, display_scheduler_stats: bool = False): """ Initialize console progress display. :param enabled: Whether to enable progress tracking and display. :param display_scheduler_stats: Whether to display scheduler statistics. """ - BenchmarkerProgress.__init__(self, enabled=enabled) + BenchmarkerProgress.__init__(self) Live.__init__( self, refresh_per_second=4, @@ -432,7 +187,9 @@ async def on_benchmark_start(self, strategy: SchedulingStrategy): self._sync_run_progress() async def on_benchmark_update( - self, aggregator_update: AggregatorState | None, scheduler_state: SchedulerState + self, + aggregator_update: EstimatedBenchmarkState | None, + scheduler_state: SchedulerState, ): """ Update display with current benchmark progress. @@ -545,7 +302,9 @@ def start_benchmark(self, strategy: SchedulingStrategy): ) def update_benchmark( - self, aggregator_update: AggregatorState, scheduler_state: SchedulerState + self, + aggregator_update: EstimatedBenchmarkState, + scheduler_state: SchedulerState, ): self.benchmark_task_states[self.current_index].update( aggregator_update, scheduler_state @@ -800,71 +559,75 @@ def start(self, strategy: SchedulingStrategy): self.strategy_type = strategy.type_ def update( - self, aggregator_update: AggregatorState, scheduler_state: SchedulerState + self, + estimated_state: EstimatedBenchmarkState, + scheduler_state: SchedulerState, ): self.progress = ( (1.0 - scheduler_state.remaining_fraction) if scheduler_state.remaining_fraction is not None else 0.0 ) - status: Literal["in_warmup", "in_progress", "in_cooldown"] | None = ( - "in_progress" # Need to handle requests_in_* isn't in aggregator_update - ) - if aggregator_update.get("requests_in_warmup"): - status = "in_warmup" - elif aggregator_update.get("requests_in_cooldown"): - status = "in_cooldown" self._update_processing_states( - benchmark_status=status, + benchmark_status=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_state_group, + key="status", + default=None, + ), start_time=scheduler_state.start_time, successful_requests=scheduler_state.successful_requests, cancelled_requests=scheduler_state.cancelled_requests, errored_requests=scheduler_state.errored_requests, ) self._update_request_stats( - request_concurrency=aggregator_update.get_metric( - key="requests", type_="avg", prefix="completed" + request_concurrency=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="concurrency_requests", ), - requests_per_second=aggregator_update.get_metric( - key="requests", - type_="rate", - prefix="completed", + requests_per_second=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_requests_per_second", ), - request_latency=aggregator_update.get_metric( - key="request_latency", type_="avg", prefix="completed" + request_latency=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_request_latency", ), ) self._update_token_stats( - output_tokens=aggregator_update.get_metric( - key="output_tokens", type_="avg", prefix="completed" + output_tokens=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_output_tokens_total", ), - output_tokens_rate=aggregator_update.get_metric( - key="output_tokens", type_="rate" + output_tokens_rate=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_output_tokens", ), - prompt_tokens=aggregator_update.get_metric( - key="prompt_tokens", type_="avg", prefix="completed" + prompt_tokens=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_input_tokens_total", ), - total_tokens_rate=aggregator_update.get_metric( - key="total_tokens", type_="rate" + total_tokens_rate=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_total_tokens", ), - time_to_first_token=( - aggregator_update.get_metric(key="time_to_first_token", type_="avg") + time_to_first_token=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_time_to_first_token", ), - inter_token_latency=( - aggregator_update.get_metric(key="inter_token_latency", type_="avg") + inter_token_latency=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_inter_token_latency", ), ) - if aggregator_update.get("updated_scheduler_stats"): + if estimated_state.get("updated_scheduler_stats"): self._update_system_stats( - request_targeted_start_delay=( - aggregator_update.get_metric( - key="request_targeted_start_delay", type_="avg", default=0.0 - ) + request_targeted_start_delay=estimated_state.get_metric( + group=EstimatedBenchmarkState.scheduler_state_group, + key="request_targeted_start_delay", ), - queued_time=( - aggregator_update.get_metric( - key="queued_time", type_="avg", default=0.0 - ) + queued_time=estimated_state.get_metric( + group=EstimatedBenchmarkState.scheduler_state_group, + key="queued_time", ), scheduler_overheads_time=0.0, # Need to add up metrics here ) diff --git a/src/guidellm/benchmark/scenario.py b/src/guidellm/benchmark/scenario.py deleted file mode 100644 index b53ef424..00000000 --- a/src/guidellm/benchmark/scenario.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import json -from functools import cache, wraps -from inspect import Parameter, signature -from pathlib import Path -from typing import Annotated, Any, Callable, Literal, TypeVar - -import yaml -from loguru import logger -from pydantic import BeforeValidator, Field, PositiveFloat, PositiveInt, SkipValidation - -from guidellm.backends import Backend, BackendType -from guidellm.benchmark.profile import Profile, ProfileType -from guidellm.benchmark.types import AggregatorInputT, DataInputT, ProcessorInputT -from guidellm.scheduler import StrategyType -from guidellm.utils import StandardBaseModel - -__all__ = [ - "GenerativeTextScenario", - "Scenario", - "enable_scenarios", - "get_builtin_scenarios", -] - -SCENARIO_DIR = Path(__file__).parent / "scenarios/" - - -@cache -def get_builtin_scenarios() -> list[str]: - """Returns list of builtin scenario names.""" - return [p.stem for p in SCENARIO_DIR.glob("*.json")] - - -def parse_float_list(value: str | float | list[float]) -> list[float]: - """ - Parse a comma separated string to a list of float - or convert single float list of one or pass float - list through. - """ - if isinstance(value, (int, float)): - return [value] - elif isinstance(value, list): - return value - - values = value.split(",") if "," in value else [value] - - try: - return [float(val) for val in values] - except ValueError as err: - raise ValueError( - "must be a number or comma-separated list of numbers." - ) from err - - -T = TypeVar("T", bound="Scenario") - - -class Scenario(StandardBaseModel): - """ - Parent Scenario class with common options for all benchmarking types. - """ - - target: str - - @classmethod - def get_default(cls: type[T], field: str) -> Any: - """Get default values for model fields""" - return cls.model_fields[field].default - - @classmethod - def from_file(cls: type[T], filename: Path, overrides: dict | None = None) -> T: - """ - Attempt to create a new instance of the model using - data loaded from json or yaml file. - """ - try: - with filename.open() as f: - if str(filename).endswith(".json"): - data = json.load(f) - else: # Assume everything else is yaml - data = yaml.safe_load(f) - except (json.JSONDecodeError, yaml.YAMLError) as e: - logger.error(f"Failed to parse {filename} as type {cls.__name__}") - raise ValueError(f"Error when parsing file: {filename}") from e - - data.update(overrides or {}) - return cls.model_validate(data) - - @classmethod - def from_builtin(cls: type[T], name: str, overrides: dict | None = None) -> T: - filename = SCENARIO_DIR / f"{name}.json" - - if not filename.is_file(): - raise ValueError(f"{name} is not a valid builtin scenario") - - return cls.from_file(filename, overrides) - - -class GenerativeTextScenario(Scenario): - """ - Scenario class for generative text benchmarks. - """ - - class Config: - # NOTE: This prevents errors due to unvalidatable - # types like PreTrainedTokenizerBase - arbitrary_types_allowed = True - - data: Annotated[ - DataInputT, - # BUG: See https://github.com/pydantic/pydantic/issues/9541 - SkipValidation, - ] - profile: StrategyType | ProfileType | Profile - rate: Annotated[list[PositiveFloat] | None, BeforeValidator(parse_float_list)] = ( - None - ) - random_seed: int = 42 - # Backend configuration - backend: BackendType | Backend = "openai_http" - backend_kwargs: dict[str, Any] | None = None - model: str | None = None - # Data configuration - processor: ProcessorInputT | None = None - processor_args: dict[str, Any] | None = None - data_args: dict[str, Any] | None = None - data_sampler: Literal["random"] | None = None - # Aggregators configuration - add_aggregators: AggregatorInputT | None = None - warmup: Annotated[float | None, Field(gt=0, le=1)] = None - cooldown: Annotated[float | None, Field(gt=0, le=1)] = None - request_samples: PositiveInt | None = 20 - # Constraints configuration - max_seconds: PositiveFloat | PositiveInt | None = None - max_requests: PositiveInt | None = None - max_errors: PositiveInt | None = None - max_error_rate: PositiveFloat | None = None - max_global_error_rate: PositiveFloat | None = None - - -# Decorator function to apply scenario to a function -def enable_scenarios(func: Callable) -> Any: - @wraps(func) - async def decorator(*args, scenario: Scenario | None = None, **kwargs) -> Any: - if scenario is not None: - kwargs.update(scenario.model_dump()) - return await func(*args, **kwargs) - - # Modify the signature of the decorator to include the `scenario` argument - sig = signature(func) - params = list(sig.parameters.values()) - # Place `scenario` before `**kwargs` or any parameter with a default value - loc = next( - ( - i - for i, p in enumerate(params) - if p.kind is Parameter.VAR_KEYWORD or p.default is not Parameter.empty - ), - len(params), - ) - params.insert( - loc, - Parameter( - "scenario", - Parameter.POSITIONAL_OR_KEYWORD, - default=None, - annotation=Scenario | None, - ), - ) - decorator.__signature__ = sig.replace(parameters=params) # type: ignore [attr-defined] - - return decorator diff --git a/src/guidellm/benchmark/scenarios/__init__.py b/src/guidellm/benchmark/scenarios/__init__.py index e69de29b..030f9bbd 100644 --- a/src/guidellm/benchmark/scenarios/__init__.py +++ b/src/guidellm/benchmark/scenarios/__init__.py @@ -0,0 +1,40 @@ +""" +Builtin benchmark scenario definitions and discovery utilities. + +This module provides access to predefined benchmark scenarios stored as JSON files +within the scenarios directory. It enables discovery and retrieval of builtin +scenarios by name or filename, supporting both stem names (without extension) and +full filenames for flexible scenario loading. +""" + +from __future__ import annotations + +from functools import cache +from pathlib import Path +from typing import Annotated + +__all__ = ["SCENARIO_DIR", "get_builtin_scenarios"] + +SCENARIO_DIR: Annotated[ + Path, + "Directory path containing builtin scenario JSON files", +] = Path(__file__).parent + + +@cache +def get_builtin_scenarios() -> dict[str, Path]: + """ + Retrieve all builtin scenario definitions from the scenarios directory. + + Scans the scenarios directory for JSON files and returns a mapping of scenario + names to their file paths. Each scenario is indexed by both its stem name + (filename without extension) and full filename for convenient lookup. + + :return: Dictionary mapping scenario names and filenames to their Path objects + """ + builtin = {} + for path in SCENARIO_DIR.glob("*.json"): + builtin[path.stem] = path + builtin[path.name] = path + + return builtin diff --git a/src/guidellm/benchmark/scenarios/chat.json b/src/guidellm/benchmark/scenarios/chat.json index 7ed4ce16..58fd18e2 100644 --- a/src/guidellm/benchmark/scenarios/chat.json +++ b/src/guidellm/benchmark/scenarios/chat.json @@ -1,4 +1,6 @@ { "profile": "sweep", - "data": "prompt_tokens=512,prompt_tokens_stdev=128,prompt_tokens_min=1,prompt_tokens_max=1024,output_tokens=256,output_tokens_stdev=64,output_tokens_min=1,output_tokens_max=1024" -} + "data": [ + "prompt_tokens=512,prompt_tokens_stdev=128,prompt_tokens_min=1,prompt_tokens_max=1024,output_tokens=256,output_tokens_stdev=64,output_tokens_min=1,output_tokens_max=1024" + ] +} \ No newline at end of file diff --git a/src/guidellm/benchmark/scenarios/rag.json b/src/guidellm/benchmark/scenarios/rag.json index d790ce60..ea38d76e 100644 --- a/src/guidellm/benchmark/scenarios/rag.json +++ b/src/guidellm/benchmark/scenarios/rag.json @@ -1,4 +1,6 @@ { "profile": "sweep", - "data": "prompt_tokens=4096,prompt_tokens_stdev=512,prompt_tokens_min=2048,prompt_tokens_max=6144,output_tokens=512,output_tokens_stdev=128,output_tokens_min=1,output_tokens_max=1024" -} + "data": [ + "prompt_tokens=4096,prompt_tokens_stdev=512,prompt_tokens_min=2048,prompt_tokens_max=6144,output_tokens=512,output_tokens_stdev=128,output_tokens_min=1,output_tokens_max=1024" + ] +} \ No newline at end of file diff --git a/src/guidellm/benchmark/schemas.py b/src/guidellm/benchmark/schemas.py new file mode 100644 index 00000000..9fd09461 --- /dev/null +++ b/src/guidellm/benchmark/schemas.py @@ -0,0 +1,2076 @@ +""" +Benchmark data models and metrics for generative AI performance measurement. + +Provides comprehensive data structures for capturing, storing, and analyzing +benchmark results from scheduler-driven generative AI workload executions. +Core abstractions include base benchmark interfaces, generative-specific +metrics with token/latency distributions, request-level statistics tracking, +and multi-benchmark reporting capabilities. These models enable detailed +performance analysis including throughput, latency, concurrency patterns, and +domain-specific metrics for text, image, video, and audio generation tasks. +""" + +from __future__ import annotations + +import json +import random +import time +import uuid +from abc import ABC, abstractmethod +from collections.abc import Callable, Iterable +from pathlib import Path +from typing import Any, ClassVar, Literal, TypeVar, cast + +import yaml +from pydantic import ConfigDict, Field, computed_field, model_serializer +from torch.utils.data import Sampler +from transformers import PreTrainedTokenizerBase + +from guidellm.backends import Backend, BackendType +from guidellm.benchmark.profile import Profile, ProfileType +from guidellm.benchmark.scenarios import get_builtin_scenarios +from guidellm.data import DatasetPreprocessor +from guidellm.scheduler import ( + BackendInterface, + Environment, + SchedulerState, + SchedulingStrategy, + StrategyType, +) +from guidellm.schemas import ( + GenerationRequest, + GenerationResponse, + GenerativeRequestStats, + RequestInfo, + UsageMetrics, +) +from guidellm.utils import ( + InfoMixin, + StandardBaseDict, + StandardBaseModel, + StatusBreakdown, + StatusDistributionSummary, +) + +__all__ = [ + "Benchmark", + "BenchmarkGenerativeTextArgs", + "BenchmarkSchedulerStats", + "BenchmarkT", + "BenchmarkerArgs", + "BenchmarkerDict", + "EstimatedBenchmarkState", + "GenerativeAudioMetricsSummary", + "GenerativeBenchmark", + "GenerativeBenchmarksReport", + "GenerativeImageMetricsSummary", + "GenerativeMetrics", + "GenerativeMetricsSummary", + "GenerativeTextMetricsSummary", + "GenerativeVideoMetricsSummary", + "SchedulerDict", +] + + +class EstimatedBenchmarkState(dict[str, Any]): + """ + Accumulator for real-time benchmark metrics during scheduler execution. + + Tracks incremental metrics, running averages, and time-based statistics as + requests are processed. Maintains grouped metrics for benchmark state, + benchmark-level metrics, and scheduler-level metrics with support for + average, rate, and time-averaged metric calculations. + + :cvar benchmark_state_group: Metric group key for benchmark state tracking + :cvar benchmark_metrics_group: Metric group key for benchmark-level metrics + :cvar scheduler_state_group: Metric group key for scheduler-level metrics + """ + + benchmark_state_group: ClassVar[Literal["benchmark_state"]] = "benchmark_state" + benchmark_metrics_group: ClassVar[Literal["benchmark_metrics"]] = ( + "benchmark_metrics" + ) + scheduler_state_group: ClassVar[Literal["scheduler_state"]] = "scheduler_state" + + def get_metric( + self, + group: str, + key: str, + default: int | float | None = None, + ) -> int | float | None: + """ + Retrieve a grouped metric value by group and key. + + :param group: Metric group identifier + :param key: Metric key within the group + :param default: Value returned if metric doesn't exist + :return: The metric value or default if not found + """ + return self.get(f"{group}_{key}", default) + + def set_metric( + self, + group: str, + key: str, + value: bool | int | float | None, + start_val: bool | int | float | None = None, + ) -> bool | int | float | None: + """ + Set a grouped metric value, optionally adjusting by a starting value. + + :param group: Metric group identifier + :param key: Metric key within the group + :param value: Metric value to set + :param start_val: Optional starting value to subtract from the metric value + :return: The adjusted metric value or None if value is None + """ + if value is None: + return None + + if start_val is not None: + value -= start_val + self[f"{group}_{key}"] = value + + return value + + def add_avg_metric( + self, + group: str, + key: str, + value: bool | int | float | None, + start_val: bool | int | float | None = 0.0, + count: int | None = 1, + ): + """ + Add a value to a running average metric calculation. + + :param group: Metric group identifier + :param key: Metric key within the group + :param value: Value to add to the average + :param start_val: Optional starting value to subtract before adding + :param count: Number of observations this value represents + """ + if value is None or count is None: + return + + if start_val is not None: + value -= start_val + + total_key = f"{group}_{key}_total" + count_key = f"{group}_{key}_count" + self[total_key] = self.get(total_key, 0) + value + self[count_key] = self.get(count_key, 0) + count + + average = self[total_key] / self[count_key] if self[count_key] > 0 else 0.0 + self.set_metric( + group=group, + key=key, + value=average, + ) + + def add_avg_rate_metric( + self, + group: str, + key: str, + value: bool | int | float | None, + start_val: bool | int | float | None = 0.0, + start_time: float | None = None, + end_time: float | None = None, + numerator_type: Literal["avg", "total", "count"] = "total", + ): + """ + Add a value to a rate-based average metric calculation. + + :param group: Metric group identifier + :param key: Metric key within the group + :param value: Value to add to the average + :param start_val: Optional starting value to subtract before adding + :param start_time: Start time for rate calculation, defaults to current time + :param end_time: End time for rate calculation, defaults to current time + :param numerator_type: Type of numerator for rate calculation + """ + if value is None: + return + + self.add_avg_metric( + group=group, + key=key, + value=value, + start_val=start_val, + ) + start_time_key = f"{group}_{key}_start_time" + if self.get(start_time_key) is None: + if start_time is None: + start_time = time.time() + self[start_time_key] = start_time + else: + self[start_time_key] = start_time or self[start_time_key] + + end_time = end_time or time.time() + elapsed_time = end_time - self[start_time_key] + + if elapsed_time > 0: + numerator_key = ( + f"{group}_{key}_{numerator_type}" + if numerator_type != "avg" + else f"{group}_{key}" + ) + rate = self[numerator_key] / elapsed_time + self.set_metric( + group=group, + key=f"{key}_per_second", + value=rate, + ) + + def add_time_averaged_metric( + self, + group: str, + key: str, + value: bool | int | float | None, + recorded_time: float | None = None, + ): + """ + Add a value to a time-weighted average metric calculation. + + :param group: Metric group identifier + :param key: Metric key within the group + :param value: Value to add to the time-weighted average + :param recorded_time: Time of the observation, defaults to current time + """ + if value is None: + return + + if recorded_time is None: + recorded_time = time.time() + + time_avg_numerator_key = f"{group}_{key}_time_avg_numerator" + time_avg_denominator_key = f"{group}_{key}_time_avg_denominator" + last_recorded_time_key = f"{group}_{key}_last_recorded_time" + last_recorded_value_key = f"{group}_{key}_last_recorded_value" + + if last_recorded_time_key not in self: + self[last_recorded_time_key] = recorded_time + self[last_recorded_value_key] = value + self[time_avg_numerator_key] = value + self[time_avg_denominator_key] = 0.0 + else: + time_delta = recorded_time - self[last_recorded_time_key] + self[time_avg_numerator_key] += self[last_recorded_value_key] * time_delta + self[time_avg_denominator_key] += time_delta + self[last_recorded_time_key] = recorded_time + self[last_recorded_value_key] = value + + if self[time_avg_denominator_key] > 0: + average = self[time_avg_numerator_key] / self[time_avg_denominator_key] + else: + average = value + + self.set_metric( + group=group, + key=key, + value=average, + ) + + +class BenchmarkerArgs(StandardBaseDict): + """ + Configuration parameters for benchmark execution and request sampling. + + Defines run identification, request sampling strategy, warmup/cooldown phases, + and metric preferences for benchmark executions. Provides methods to determine + whether a request falls within warmup or cooldown periods based on time, + request count, or percentage-based thresholds. + """ + + run_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique identifier for the benchmark run", + ) + run_index: int = Field(default=0, description="Index of the benchmark run") + sample_requests: int | None = Field( + default=20, + description=( + "Number of requests to sample and keep in the final benchmark for metrics" + ), + ) + warmup: int | float | None = Field( + default=None, description="Warmup time before benchmarking starts" + ) + cooldown: int | float | None = Field( + default=None, description="Cooldown time after benchmarking ends" + ) + prefer_response_metrics: bool = Field( + default=True, + description="Whether to prefer response metrics over request metrics", + ) + + def is_in_warmup( + self, request_info: RequestInfo, scheduler_state: SchedulerState + ) -> bool: + """ + Check if a request is in the warmup phase. + + :param request_info: Information about the current request + :param scheduler_state: Current state of the scheduler + :return: True if the request is in warmup phase, False otherwise + """ + if self.warmup is not None and 0 < self.warmup < 1: + # Percentage-based warmup + return ( + scheduler_state.remaining_fraction is not None + and scheduler_state.remaining_fraction > (1 - self.warmup) + ) + + if self.warmup is not None and self.warmup > 1: + # Count/time-based warmup + if scheduler_state.processed_requests < self.warmup: + return True + + current_time = request_info.timings.targeted_start + return ( + current_time is not None + and (current_time - scheduler_state.start_time) < self.warmup + ) + + return False + + def is_in_cooldown( + self, request_info: RequestInfo, scheduler_state: SchedulerState + ) -> bool: + """ + Check if a request is in the cooldown phase. + + :param request_info: Information about the current request + :param scheduler_state: Current state of the scheduler + :return: True if the request is in cooldown phase, False otherwise + """ + if self.cooldown is not None and 0 < self.cooldown < 1: + # Percentage-based cooldown + return ( + scheduler_state.remaining_fraction is not None + and scheduler_state.remaining_fraction < self.cooldown + ) + + if self.cooldown is not None and self.cooldown > 1: + # Count/time-based cooldown + if ( + scheduler_state.remaining_requests is not None + and scheduler_state.remaining_requests <= self.cooldown + ): + return True + + current_time = ( + request_info.timings.resolve_end or request_info.timings.targeted_start + ) + return ( + current_time is not None + and scheduler_state.remaining_duration is not None + and scheduler_state.remaining_duration < self.cooldown + ) + + return False + + +class Benchmark(ABC): + """ + Abstract base interface for benchmark result implementations. + + Defines the contract for benchmark classes to provide run metrics sampling, + request metrics sampling, real-time estimate updates, and final compilation + of benchmark results from scheduler execution data. + """ + + @abstractmethod + def get_run_metrics_sample( + self, + ) -> dict[Literal["start_time", "end_time", "duration"], float]: + """ + Get a sample of run-level timing metrics. + + :return: Dictionary containing start_time, end_time, and duration metrics + """ + ... + + @abstractmethod + def get_request_metrics_sample( + self, + ) -> dict[ + Literal[ + "request_count", + "request_latency", + "request_throughput", + "request_concurrency", + ], + float, + ]: + """ + Get a sample of request-level performance metrics. + + :return: Dictionary containing request count, latency, throughput, and + concurrency metrics + """ + ... + + @classmethod + @abstractmethod + def update_estimate( + cls, + args: BenchmarkerArgs, + state: EstimatedBenchmarkState, + response: Any, + request: Any, + request_info: RequestInfo, + scheduler_state: SchedulerState, + ): + """ + Update real-time benchmark estimates with new request data. + + :param args: Benchmark configuration arguments + :param state: Current estimated benchmark state to update + :param response: Response received from the backend + :param request: Original request sent to the backend + :param request_info: Metadata about the request execution + :param scheduler_state: Current state of the scheduler + """ + ... + + @classmethod + @abstractmethod + def compile( + cls, + args: BenchmarkerArgs, + estimated_state: EstimatedBenchmarkState, + scheduler_state: SchedulerState, + profile: Profile, + requests: Iterable, + backend: BackendInterface, + environment: Environment, + strategy: SchedulingStrategy, + constraints: dict[str, dict[str, Any]], + ) -> Any: + """ + Compile final benchmark results from accumulated state. + + :param args: Benchmark configuration arguments + :param estimated_state: Accumulated benchmark state from execution + :param scheduler_state: Final state of the scheduler + :param profile: Benchmark profile configuration + :param requests: Collection of requests executed + :param backend: Backend interface used for execution + :param environment: Execution environment configuration + :param strategy: Scheduling strategy used + :param constraints: Execution constraints applied + :return: Compiled benchmark results instance + """ + ... + + +BenchmarkT = TypeVar("BenchmarkT", bound=Benchmark) + + +class BenchmarkSchedulerStats(StandardBaseDict): + """Scheduler timing and performance statistics.""" + + group_name: ClassVar[Literal["scheduler_stats"]] = "scheduler_stats" + + start_time: float = Field( + description="Unix timestamp when the benchmark run started" + ) + end_time: float = Field(description="Unix timestamp when the benchmark run ended") + requests_made: StatusBreakdown[int, int, int, int] = Field( + description="Request counts by status: successful, incomplete, errored, total" + ) + queued_time_avg: float = Field( + description="Avg time requests spent in the queue (seconds)" + ) + worker_resolve_start_delay_avg: float = Field( + description="Avg delay before worker begins resolving req after dequeue (sec)" + ) + worker_resolve_time_avg: float = Field( + description="Avg time for worker to resolve requests (seconds)" + ) + worker_resolve_end_delay_avg: float = Field( + description="Avg delay after request end till worker resolves (seconds)" + ) + finalized_delay_avg: float = Field( + description="Avg delay after resolve til finalized with in scheduler (sec)" + ) + worker_targeted_start_delay_avg: float = Field( + description="Avg delay from targeted start to actual worker start (seconds)" + ) + request_start_delay_avg: float = Field( + description="Avg delay after resolve til request start (seconds)" + ) + request_time_avg: float = Field(description="Avg request processing time (seconds)") + request_targeted_start_delay_avg: float = Field( + description="Avg delay from targeted start to actual request start" + ) + + @classmethod + def update_estimate(cls, state: EstimatedBenchmarkState, request_info: RequestInfo): + """ + Update estimated scheduler statistics with request timing information. + + :param state: Current estimated benchmark state to update + :param request_info: Metadata about the request execution with timing data + """ + state.set_metric(group=cls.group_name, key="updated", value=True) + state.add_avg_metric( + group=cls.group_name, + key="queued_time", + value=request_info.timings.dequeued, + start_val=request_info.timings.queued, + ) + state.add_avg_metric( + group=cls.group_name, + key="worker_resolve_start_delay", + value=request_info.timings.resolve_start, + start_val=request_info.timings.scheduled_at, + ) + state.add_avg_metric( + group=cls.group_name, + key="worker_resolve_time", + value=request_info.timings.resolve_end, + start_val=request_info.timings.resolve_start, + ) + state.add_avg_metric( + group=cls.group_name, + key="worker_resolve_end_delay", + value=request_info.timings.request_end, + start_val=request_info.timings.resolve_end, + ) + state.add_avg_metric( + group=cls.group_name, + key="finalized_delay", + value=request_info.timings.finalized, + start_val=request_info.timings.resolve_end, + ) + state.add_avg_metric( + group=cls.group_name, + key="worker_targeted_start_delay", + value=request_info.timings.resolve_start, + start_val=request_info.timings.targeted_start, + ) + state.add_avg_metric( + group=cls.group_name, + key="request_start_delay", + value=request_info.timings.request_start, + start_val=request_info.timings.resolve_start, + ) + state.add_avg_metric( + group=cls.group_name, + key="request_time", + value=request_info.timings.request_end, + start_val=request_info.timings.request_start, + ) + state.add_avg_metric( + group=cls.group_name, + key="request_targeted_start_delay", + value=request_info.timings.request_start, + start_val=request_info.timings.targeted_start, + ) + + @classmethod + def compile( + cls, estimated_state: EstimatedBenchmarkState, scheduler_state: SchedulerState + ) -> BenchmarkSchedulerStats: + """ + Compile final scheduler statistics from accumulated state. + + :param estimated_state: Accumulated benchmark state with scheduler metrics + :param scheduler_state: Final state of the scheduler + :return: Compiled scheduler statistics instance + """ + return BenchmarkSchedulerStats( + start_time=scheduler_state.start_time, + end_time=scheduler_state.end_time or scheduler_state.start_time, + requests_made=StatusBreakdown[int, int, int, int]( + successful=scheduler_state.successful_requests, + incomplete=scheduler_state.cancelled_requests, + errored=scheduler_state.errored_requests, + total=( + scheduler_state.successful_requests + + scheduler_state.cancelled_requests + + scheduler_state.errored_requests + ), + ), + queued_time_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="queued_time", default=-1.0 + ), + ), + worker_resolve_start_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="worker_resolve_start_delay", default=-1.0 + ), + ), + worker_resolve_time_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="worker_resolve_time", default=-1.0 + ), + ), + worker_resolve_end_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="worker_resolve_end_delay", default=-1.0 + ), + ), + finalized_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="finalized_delay", default=-1.0 + ), + ), + worker_targeted_start_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, + key="worker_targeted_start_delay", + default=-1.0, + ), + ), + request_start_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="request_start_delay", default=-1.0 + ), + ), + request_time_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="request_time", default=-1.0 + ), + ), + request_targeted_start_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, + key="request_targeted_start_delay", + default=-1.0, + ), + ), + ) + + +class GenerativeMetricsSummary(StandardBaseDict): + """ + Statistical summaries for input, output, and total metrics. + + Provides distribution summaries across successful, incomplete, and errored + requests for absolute values, per-second rates, and concurrency levels. + """ + + input: StatusDistributionSummary = Field( + description="Distribution of input metric values" + ) + input_per_second: StatusDistributionSummary = Field( + description="Distribution of input metric rates per second" + ) + input_concurrency: StatusDistributionSummary = Field( + description="Distribution of concurrent input metric values" + ) + + output: StatusDistributionSummary = Field( + description="Distribution of output metric values" + ) + output_per_second: StatusDistributionSummary = Field( + description="Distribution of output metric rates per second" + ) + output_concurrency: StatusDistributionSummary = Field( + description="Distribution of concurrent output metric values" + ) + + total: StatusDistributionSummary = Field( + description="Distribution of total metric values (input + output)" + ) + total_per_second: StatusDistributionSummary = Field( + description="Distribution of total metric rates per second" + ) + total_concurrency: StatusDistributionSummary = Field( + description="Distribution of concurrent total metric values" + ) + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_values: list[int | float], + output_values: list[int | float], + ) -> GenerativeMetricsSummary: + """ + Compile generative metrics summary from request data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_values: Input metric values for each request + :param output_values: Output metric values for each request + :return: Compiled generative metrics summary + """ + total_values = [ + input_val + output_val + for input_val, output_val in zip(input_values, output_values, strict=False) + ] + + return GenerativeMetricsSummary( + input=StatusDistributionSummary.from_values( + value_types=request_types, + values=input_values, + ), + input_per_second=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="rate", + weights=input_values, + ), + input_concurrency=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="concurrency", + weights=input_values, + ), + output=StatusDistributionSummary.from_values( + value_types=request_types, + values=output_values, + ), + output_per_second=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="rate", + weights=output_values, + ), + output_concurrency=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="concurrency", + weights=output_values, + ), + total=StatusDistributionSummary.from_values( + value_types=request_types, + values=total_values, + ), + total_per_second=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="rate", + weights=total_values, + ), + total_concurrency=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="concurrency", + weights=total_values, + ), + ) + + +class GenerativeTextMetricsSummary(StandardBaseDict): + """ + Text-specific metric summaries for generative benchmarks. + + Tracks token, word, and character-level metrics across input, output, and + total usage for text generation workloads. + """ + + tokens: GenerativeMetricsSummary = Field( + description="Token count metrics and distributions" + ) + words: GenerativeMetricsSummary = Field( + description="Word count metrics and distributions" + ) + characters: GenerativeMetricsSummary = Field( + description="Character count metrics and distributions" + ) + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_metrics: list[UsageMetrics], + output_metrics: list[UsageMetrics], + ) -> GenerativeTextMetricsSummary: + """ + Compile text metrics summary from request usage data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_metrics: Input usage metrics for each request + :param output_metrics: Output usage metrics for each request + :return: Compiled text metrics summary + """ + return GenerativeTextMetricsSummary( + tokens=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.text_tokens or 0 for metrics in input_metrics], + output_values=[metrics.text_tokens or 0 for metrics in output_metrics], + ), + words=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.text_words or 0 for metrics in input_metrics], + output_values=[metrics.text_words or 0 for metrics in output_metrics], + ), + characters=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[ + metrics.text_characters or 0 for metrics in input_metrics + ], + output_values=[ + metrics.text_characters or 0 for metrics in output_metrics + ], + ), + ) + + +class GenerativeImageMetricsSummary(StandardBaseDict): + """ + Image-specific metric summaries for generative benchmarks. + + Tracks token, image count, pixel, and byte-level metrics across input, output, + and total usage for image generation workloads. + """ + + tokens: GenerativeMetricsSummary = Field( + description="Image token count metrics and distributions" + ) + images: GenerativeMetricsSummary = Field( + description="Image count metrics and distributions" + ) + pixels: GenerativeMetricsSummary = Field( + description="Pixel count metrics and distributions" + ) + bytes: GenerativeMetricsSummary = Field( + description="Byte size metrics and distributions" + ) + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_metrics: list[UsageMetrics], + output_metrics: list[UsageMetrics], + ) -> GenerativeImageMetricsSummary: + """ + Compile image metrics summary from request usage data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_metrics: Input usage metrics for each request + :param output_metrics: Output usage metrics for each request + :return: Compiled image metrics summary + """ + return GenerativeImageMetricsSummary( + tokens=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.image_tokens or 0 for metrics in input_metrics], + output_values=[metrics.image_tokens or 0 for metrics in output_metrics], + ), + images=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.image_count or 0 for metrics in input_metrics], + output_values=[metrics.image_count or 0 for metrics in output_metrics], + ), + pixels=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.image_pixels or 0 for metrics in input_metrics], + output_values=[metrics.image_pixels or 0 for metrics in output_metrics], + ), + bytes=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.image_bytes or 0 for metrics in input_metrics], + output_values=[metrics.image_bytes or 0 for metrics in output_metrics], + ), + ) + + +class GenerativeVideoMetricsSummary(StandardBaseDict): + """ + Video-specific metric summaries for generative benchmarks. + + Tracks token, frame count, duration, and byte-level metrics across input, + output, and total usage for video generation workloads. + """ + + tokens: GenerativeMetricsSummary = Field( + description="Video token count metrics and distributions" + ) + frames: GenerativeMetricsSummary = Field( + description="Frame count metrics and distributions" + ) + seconds: GenerativeMetricsSummary = Field( + description="Duration metrics in seconds and distributions" + ) + bytes: GenerativeMetricsSummary = Field( + description="Byte size metrics and distributions" + ) + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_metrics: list[UsageMetrics], + output_metrics: list[UsageMetrics], + ) -> GenerativeVideoMetricsSummary: + """ + Compile video metrics summary from request usage data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_metrics: Input usage metrics for each request + :param output_metrics: Output usage metrics for each request + :return: Compiled video metrics summary + """ + return GenerativeVideoMetricsSummary( + tokens=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.video_tokens or 0 for metrics in input_metrics], + output_values=[metrics.video_tokens or 0 for metrics in output_metrics], + ), + frames=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.video_frames or 0 for metrics in input_metrics], + output_values=[metrics.video_frames or 0 for metrics in output_metrics], + ), + seconds=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.video_seconds or 0 for metrics in input_metrics], + output_values=[ + metrics.video_seconds or 0 for metrics in output_metrics + ], + ), + bytes=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.video_bytes or 0 for metrics in input_metrics], + output_values=[metrics.video_bytes or 0 for metrics in output_metrics], + ), + ) + + +class GenerativeAudioMetricsSummary(StandardBaseDict): + """ + Audio-specific metric summaries for generative benchmarks. + + Tracks token, sample count, duration, and byte-level metrics across input, + output, and total usage for audio generation workloads. + """ + + tokens: GenerativeMetricsSummary = Field( + description="Audio token count metrics and distributions" + ) + samples: GenerativeMetricsSummary = Field( + description="Sample count metrics and distributions" + ) + seconds: GenerativeMetricsSummary = Field( + description="Duration metrics in seconds and distributions" + ) + bytes: GenerativeMetricsSummary = Field( + description="Byte size metrics and distributions" + ) + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_metrics: list[UsageMetrics], + output_metrics: list[UsageMetrics], + ) -> GenerativeAudioMetricsSummary: + """ + Compile audio metrics summary from request usage data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_metrics: Input usage metrics for each request + :param output_metrics: Output usage metrics for each request + :return: Compiled audio metrics summary + """ + return GenerativeAudioMetricsSummary( + tokens=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.audio_tokens or 0 for metrics in input_metrics], + output_values=[metrics.audio_tokens or 0 for metrics in output_metrics], + ), + samples=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.audio_samples or 0 for metrics in input_metrics], + output_values=[ + metrics.audio_samples or 0 for metrics in output_metrics + ], + ), + seconds=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.audio_seconds or 0 for metrics in input_metrics], + output_values=[ + metrics.audio_seconds or 0 for metrics in output_metrics + ], + ), + bytes=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.audio_bytes or 0 for metrics in input_metrics], + output_values=[metrics.audio_bytes or 0 for metrics in output_metrics], + ), + ) + + +class GenerativeMetrics(StandardBaseDict): + """Comprehensive metrics for generative AI benchmarks.""" + + # Request stats + requests_per_second: StatusDistributionSummary = Field( + description="Distribution of requests per second across benchmark execution" + ) + request_concurrency: StatusDistributionSummary = Field( + description="Distribution of concurrent request counts during execution" + ) + request_latency: StatusDistributionSummary = Field( + description="Distribution of request latencies for completed requests" + ) + request_streaming_iterations_count: StatusDistributionSummary = Field( + description="Distribution of stream iterations for completed requests" + ) + + # General token stats + prompt_token_count: StatusDistributionSummary = Field( + description="Distribution of prompt token counts by request status" + ) + output_token_count: StatusDistributionSummary = Field( + description="Distribution of output token counts by request status" + ) + total_token_count: StatusDistributionSummary = Field( + description="Distribution of total token counts by request status" + ) + time_to_first_token_ms: StatusDistributionSummary = Field( + description="Distribution of first token latencies in milliseconds" + ) + time_per_output_token_ms: StatusDistributionSummary = Field( + description="Distribution of average time per output token in milliseconds" + ) + inter_token_latency_ms: StatusDistributionSummary = Field( + description="Distribution of inter-token latencies in milliseconds" + ) + output_tokens_wo_first_per_iteration: StatusDistributionSummary = Field( + description=( + "Distribution of output tokens (without first) generated per " + "streaming iteration" + ) + ) + output_tokens_per_second: StatusDistributionSummary = Field( + description="Distribution of output token generation rates" + ) + output_tokens_per_iteration: StatusDistributionSummary = Field( + description="Distribution of output tokens generated per streaming iteration" + ) + tokens_per_second: StatusDistributionSummary = Field( + description="Distribution of total token throughput including prompt and output" + ) + + # Domain specific stats + text: GenerativeTextMetricsSummary = Field( + description="Text-specific metrics for tokens, words, and characters" + ) + image: GenerativeImageMetricsSummary = Field( + description="Image-specific metrics for tokens, images, pixels, and bytes" + ) + video: GenerativeVideoMetricsSummary = Field( + description="Video-specific metrics for tokens, frames, duration, and bytes" + ) + audio: GenerativeAudioMetricsSummary = Field( + description="Audio-specific metrics for tokens, samples, duration, and bytes" + ) + + @classmethod + def update_estimate( + cls, + state: EstimatedBenchmarkState, + response: GenerationResponse | None, + request: GenerationRequest, + request_info: RequestInfo, + scheduler_state: SchedulerState, + ): + """ + Update real-time generative metrics estimates with new request data. + + :param state: Current estimated benchmark state to update + :param response: Response received from the backend + :param request: Original request sent to the backend + :param request_info: Metadata about the request execution + :param scheduler_state: Current state of the scheduler + """ + benchmark_start_time = scheduler_state.start_time + request_start_time = ( + request_info.timings.request_start or request_info.timings.resolve_start + ) + request_end_time = ( + request_info.timings.request_end or request_info.timings.resolve_end + ) + event_occurence_time = ( + request_info.timings.queued + if request_info.status == "queued" + else ( + request_info.timings.dequeued + if request_info.status == "pending" + else request_start_time + if request_info.status == "in_progress" + else request_end_time + ) + ) + benchmark_duration = ( + event_occurence_time - benchmark_start_time + if event_occurence_time + else None + ) + request_duration = ( + request_end_time - request_start_time if request_end_time else None + ) + + # Always track concurrency + if event_occurence_time is not None: + state.add_time_averaged_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="concurrency_requests", + value=scheduler_state.processing_requests, + recorded_time=event_occurence_time, + ) + + if request_info.status not in {"completed", "errored", "cancelled"}: + return + + state.set_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="updated", + value=True, + ) + + for prefix in (request_info.status, "total"): + requests_count = ( + scheduler_state.successful_requests + if prefix == "completed" + else scheduler_state.errored_requests + if prefix == "errored" + else scheduler_state.cancelled_requests + if prefix == "cancelled" + else scheduler_state.processed_requests + ) + input_tokens = ( + (response.input_metrics.total_tokens if response else None) + or request.input_metrics.total_tokens + or 0 + ) + output_tokens = ( + (response.output_metrics.total_tokens if response else None) + or request.output_metrics.total_tokens + or 0 + ) + + # Request distribution stats + state.set_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_requests", + value=requests_count, + ) + state.set_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_requests_per_second", + value=( + requests_count / benchmark_duration if benchmark_duration else None + ), + ) + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_request_latency", + value=request_duration, + ) + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_request_streaming_iterations", + value=request_info.timings.iterations or 0, + ) + + # Token iteration stats + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="output_tokens_iterations", + value=output_tokens, + count=request_info.timings.iterations or 1, + ) + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="output_tokens_wo_first_iterations", + value=output_tokens - 1 if output_tokens > 1 else 0, + count=request_info.timings.iterations or 1, + ) + + # Token metrics stats + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_time_to_first_token", + value=request_info.timings.first_iteration, + start_val=request_start_time, + ) + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_inter_token_latency", + value=request_info.timings.last_iteration, + start_val=request_info.timings.first_iteration, + count=(output_tokens or 1) - 1, + ) + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_time_per_output_token", + value=request_duration, + count=output_tokens or 0, + ) + + # Input/output throughput stats + if event_occurence_time is not None: + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_tokens", + value=input_tokens, + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="output_tokens", + value=output_tokens, + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="total_tokens", + value=input_tokens + output_tokens, + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_text_tokens", + value=( + (response.input_metrics.text_tokens if response else None) + or request.input_metrics.text_tokens + or 0 + ), + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_images", + value=( + (response.input_metrics.image_count if response else None) + or request.input_metrics.image_count + or 0 + ), + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_video_frames", + value=( + (response.input_metrics.video_frames if response else None) + or request.input_metrics.video_frames + or 0 + ), + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_audio_seconds", + value=request.input_metrics.audio_seconds or 0, + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + + @classmethod + def compile( + cls, + completed: list[GenerativeRequestStats], + errored: list[GenerativeRequestStats], + incomplete: list[GenerativeRequestStats], + ) -> GenerativeMetrics: + """ + Compile final generative metrics from request statistics. + + :param completed: Successfully completed request statistics + :param errored: Failed request statistics + :param incomplete: Incomplete/cancelled request statistics + :return: Compiled generative metrics with full distributions + """ + requests = completed + errored + incomplete + request_types = cast( + "list[Literal['successful', 'error', 'incomplete']]", + ["successful"] * len(completed) + + ["error"] * len(errored) + + ["incomplete"] * len(incomplete), + ) + request_times = [ + ( + req.info.timings.request_start or req.info.timings.resolve_start or 0, + req.info.timings.request_end or req.info.timings.resolve_end or 0, + ) + for req in requests + ] + input_metrics = [req.input_metrics for req in requests] + output_metrics = [req.output_metrics for req in requests] + + return GenerativeMetrics( + # Request stats + requests_per_second=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="rate", + ), + request_concurrency=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="concurrency", + ), + request_latency=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.request_latency or 0.0 for req in requests], + ), + request_streaming_iterations_count=StatusDistributionSummary.from_values( + value_types=request_types, + values=[float(req.info.timings.iterations or 0) for req in requests], + ), + # General token stats + prompt_token_count=StatusDistributionSummary.from_values( + value_types=request_types, + values=[float(req.prompt_tokens or 0) for req in requests], + ), + output_token_count=StatusDistributionSummary.from_values( + value_types=request_types, + values=[float(req.output_tokens or 0) for req in requests], + ), + total_token_count=StatusDistributionSummary.from_values( + value_types=request_types, + values=[float(req.total_tokens or 0) for req in requests], + ), + time_to_first_token_ms=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.time_to_first_token_ms or 0.0 for req in requests], + ), + time_per_output_token_ms=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.time_per_output_token_ms or 0.0 for req in requests], + ), + inter_token_latency_ms=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.inter_token_latency_ms or 0.0 for req in requests], + ), + output_tokens_wo_first_per_iteration=StatusDistributionSummary.from_values( + value_types=request_types, + values=[ + max(0.0, (req.output_metrics.total_tokens or 1.0) - 1.0) + for req in requests + ], + weights=[req.info.timings.iterations or 1 for req in requests], + ), + output_tokens_per_second=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.output_tokens_per_second or 0.0 for req in requests], + ), + output_tokens_per_iteration=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.output_tokens_per_iteration or 0.0 for req in requests], + weights=[req.info.timings.iterations or 1 for req in requests], + ), + tokens_per_second=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.tokens_per_second or 0.0 for req in requests], + ), + # Domain-specific stats + text=GenerativeTextMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_metrics=input_metrics, + output_metrics=output_metrics, + ), + image=GenerativeImageMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_metrics=input_metrics, + output_metrics=output_metrics, + ), + video=GenerativeVideoMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_metrics=input_metrics, + output_metrics=output_metrics, + ), + audio=GenerativeAudioMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_metrics=input_metrics, + output_metrics=output_metrics, + ), + ) + + +class SchedulerDict(StandardBaseDict): + """Scheduler configuration and execution state dictionary.""" + + strategy: SchedulingStrategy = Field( + description="Scheduling strategy used for request distribution" + ) + constraints: dict[str, dict[str, Any]] = Field( + description="Execution constraints applied during benchmarking" + ) + state: SchedulerState = Field( + description="Final state of the scheduler after execution" + ) + + +class BenchmarkerDict(StandardBaseDict): + """Benchmarker configuration and component settings dictionary.""" + + profile: Profile = Field(description="Benchmark profile configuration") + requests: dict[str, Any] = Field( + description="Request configuration and dataset information" + ) + backend: dict[str, Any] = Field( + description="Backend configuration and connection details" + ) + environment: dict[str, Any] = Field( + description="Execution environment configuration" + ) + + +class GenerativeBenchmark(Benchmark, StandardBaseDict): + """Complete generative AI benchmark results with specialized metrics.""" + + group_name: ClassVar[Literal["generative_benchmark"]] = "generative_benchmark" + + type_: Literal["generative_benchmark"] = "generative_benchmark" # type: ignore[assignment] + id_: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique identifier for this benchmark execution", + ) + run_id: str = Field( + description="Identifier for the benchmarker run containing this benchmark" + ) + run_index: int = Field( + description="Sequential index of this benchmark within the benchmarker run" + ) + scheduler: SchedulerDict = Field( + description="Scheduler configuration and execution state" + ) + benchmarker: BenchmarkerDict = Field( + description="Benchmarker configuration and component settings" + ) + run_stats: BenchmarkSchedulerStats = Field( + description="Scheduler timing and performance statistics" + ) + start_time: float = Field( + default=-1.0, description="Unix timestamp when the first request was initiated" + ) + end_time: float = Field( + default=-1.0, description="Unix timestamp when the last request completed" + ) + + def get_run_metrics_sample( + self, + ) -> dict[Literal["start_time", "end_time", "duration"], float]: + return { + "start_time": self.start_time, + "end_time": self.end_time, + "duration": self.duration, + } + + def get_request_metrics_sample( + self, + ) -> dict[ + Literal[ + "request_count", + "request_latency", + "request_throughput", + "request_concurrency", + ], + float, + ]: + return { + "request_count": self.request_totals.successful, + "request_latency": self.metrics.request_latency.successful.mean, + "request_throughput": self.metrics.requests_per_second.successful.mean, + "request_concurrency": self.metrics.request_concurrency.successful.mean, + } + + @computed_field # type: ignore[misc] + @property + def duration(self) -> float: + """ + Benchmark execution duration in seconds. + + :return: Time elapsed from first request start to last request completion. + """ + return self.end_time - self.start_time + + metrics: GenerativeMetrics = Field( + description="Performance metrics and statistical distributions" + ) + request_totals: StatusBreakdown[int, int, int, int] = Field( + description="Request counts by status: successful, incomplete, errored, total" + ) + requests: StatusBreakdown[ + list[GenerativeRequestStats], + list[GenerativeRequestStats], + list[GenerativeRequestStats], + None, + ] = Field( + description="Request details grouped by status: successful, incomplete, errored" + ) + + @classmethod + def update_estimate( + cls, + args: BenchmarkerArgs, + state: EstimatedBenchmarkState, + response: GenerationResponse | None, + request: GenerationRequest, + request_info: RequestInfo, + scheduler_state: SchedulerState, + ): + """ + Update generative benchmark estimates with new request data. + + Handles warmup/cooldown filtering, request sampling via reservoir sampling, + and delegates metric updates to child metric classes. + + :param args: Benchmark configuration arguments + :param state: Current estimated benchmark state to update + :param response: Response received from the backend + :param request: Original request sent to the backend + :param request_info: Metadata about the request execution + :param scheduler_state: Current state of the scheduler + """ + if ( + request_info.status == "cancelled" + and request_info.timings.resolve_start is None + ): + # Cancelled requests that never started should be ignored + return + + # Update child metric groups + BenchmarkSchedulerStats.update_estimate(state, request_info) + GenerativeMetrics.update_estimate( + state, response, request, request_info, scheduler_state + ) + + # Store requests and sampling info, update counts + if "requests_completed" not in state: + state["requests_completed"] = [] + state["samples_completed"] = [] + state["requests_errored"] = [] + state["samples_errored"] = [] + state["requests_incomplete"] = [] + state["samples_incomplete"] = [] + in_warmup = state.set_metric( + group=EstimatedBenchmarkState.benchmark_state_group, + key="in_warmup", + value=args.is_in_warmup(request_info, scheduler_state), + ) + in_cooldown = state.set_metric( + group=EstimatedBenchmarkState.benchmark_state_group, + key="in_cooldown", + value=args.is_in_cooldown(request_info, scheduler_state), + ) + state[f"{EstimatedBenchmarkState.benchmark_state_group}_status"] = ( + "in_cooldown" + if in_cooldown + else "in_warmup" + if in_warmup + else "in_progress" + ) + + if ( + request_info.status not in {"completed", "errored", "cancelled"} + or in_warmup + or in_cooldown + ): + # Must be fully resolved to be added + return + + state.set_metric( + group=EstimatedBenchmarkState.benchmark_state_group, + key="updated", + value=True, + ) + + if response is None: + response = GenerationResponse( + request_id=request.request_id, request_args=str(request.arguments) + ) + + stats = response.compile_stats( + request, request_info, args.prefer_response_metrics + ) + + # Determine status and get corresponding lists + if request_info.status == "completed": + requests_list = state["requests_completed"] + samples_list = state["samples_completed"] + elif request_info.status == "errored": + requests_list = state["requests_errored"] + samples_list = state["samples_errored"] + else: # cancelled (incomplete) + requests_list = state["requests_incomplete"] + samples_list = state["samples_incomplete"] + + # Add to requests list + requests_list.append(stats) + current_index = len(requests_list) - 1 + + # Handle request sampling logic + if args.sample_requests is None: + # No sampling, add index to samples list + samples_list.append(current_index) + elif args.sample_requests > 0 and len(samples_list) < args.sample_requests: + # Space in samples list, add index + samples_list.append(current_index) + elif ( + args.sample_requests > 0 + and (replace_index := random.randrange(len(requests_list))) + < args.sample_requests + ): + # No space, adding based on reservoir sampling + samples_list[replace_index] = current_index + # Sampling set to 0, don't keep any requests + + @classmethod + def compile( + cls, + args: BenchmarkerArgs, + estimated_state: EstimatedBenchmarkState, + scheduler_state: SchedulerState, + profile: Profile, + requests: Iterable, + backend: BackendInterface, + environment: Environment, + strategy: SchedulingStrategy, + constraints: dict[str, dict[str, Any]], + ) -> GenerativeBenchmark: + """ + Compile final generative benchmark from accumulated state. + + :param args: Benchmark configuration arguments + :param estimated_state: Accumulated benchmark state from execution + :param scheduler_state: Final state of the scheduler + :param profile: Benchmark profile configuration + :param requests: Collection of requests executed + :param backend: Backend interface used for execution + :param environment: Execution environment configuration + :param strategy: Scheduling strategy used + :param constraints: Execution constraints applied + :return: Compiled generative benchmark instance + """ + return GenerativeBenchmark( + run_id=args.run_id, + run_index=args.run_index, + scheduler=SchedulerDict( + strategy=strategy, + constraints={ + key: InfoMixin.extract_from_obj(val) + for key, val in constraints.items() + }, + state=scheduler_state, + ), + benchmarker=BenchmarkerDict( + profile=profile, + requests=InfoMixin.extract_from_obj(requests), + backend=backend.info, + environment=environment.info, + ), + run_stats=BenchmarkSchedulerStats.compile(estimated_state, scheduler_state), + start_time=scheduler_state.start_time or -1.0, + end_time=scheduler_state.end_time or -1.0, + metrics=GenerativeMetrics.compile( + completed=estimated_state.get("requests_completed", []), + errored=estimated_state.get("requests_errored", []), + incomplete=estimated_state.get("requests_incomplete", []), + ), + request_totals=StatusBreakdown[int, int, int, int]( + successful=len(estimated_state.get("requests_completed", [])), + incomplete=len(estimated_state.get("requests_incomplete", [])), + errored=len(estimated_state.get("requests_errored", [])), + total=( + len(estimated_state.get("requests_completed", [])) + + len(estimated_state.get("requests_incomplete", [])) + + len(estimated_state.get("requests_errored", [])) + ), + ), + requests=StatusBreakdown[ + list[GenerativeRequestStats], + list[GenerativeRequestStats], + list[GenerativeRequestStats], + None, + ]( + successful=estimated_state.get("requests_completed", []), + incomplete=estimated_state.get("requests_incomplete", []), + errored=estimated_state.get("requests_errored", []), + total=None, + ), + ) + + +class BenchmarkGenerativeTextArgs(StandardBaseModel): + """ + Configuration arguments for generative text benchmark execution. + + Defines all parameters for benchmark setup including target endpoint, data + sources, backend configuration, processing pipeline, output formatting, and + execution constraints. Supports loading from scenario files and merging with + runtime overrides. + """ + + @classmethod + def create( + cls, scenario: Path | str | None, **kwargs: dict[str, Any] + ) -> BenchmarkGenerativeTextArgs: + """ + Create benchmark args from scenario file and/or keyword arguments. + + :param scenario: Path to scenario file or name of built-in scenario + :param kwargs: Additional keyword arguments to override scenario values + :return: Configured benchmark args instance + :raises ValueError: If scenario is not found or file format is unsupported + """ + constructor_kwargs = {} + + if scenario is not None: + if isinstance(scenario, str) and scenario in ( + builtin_scenarios := get_builtin_scenarios() + ): + scenario_path = builtin_scenarios[scenario] + elif Path(scenario).exists() and Path(scenario).is_file(): + scenario_path = Path(scenario) + else: + raise ValueError(f"Scenario '{scenario}' not found.") + + with scenario_path.open() as file: + if scenario_path.suffix == ".json": + scenario_data = json.load(file) + elif scenario_path.suffix in {".yaml", ".yml"}: + scenario_data = yaml.safe_load(file) + else: + raise ValueError( + f"Unsupported scenario file format: {scenario_path.suffix}" + ) + if "args" in scenario_data: + # loading from a report file + scenario_data = scenario_data["args"] + constructor_kwargs.update(scenario_data) + + for key, value in kwargs.items(): + if value != cls.get_default(key): + constructor_kwargs[key] = value + + return cls.model_validate(constructor_kwargs) + + @classmethod + def get_default(cls: BenchmarkGenerativeTextArgs, field: str) -> Any: + """ + Get default value for a model field. + + :param field: Name of the field to retrieve default for + :return: Default value for the specified field + :raises ValueError: If field is not found in model + """ + if field not in BenchmarkGenerativeTextArgs.model_fields: + raise ValueError( + f"Field '{field}' not found in BenchmarkGenerativeTextArgs" + ) + + field_info = BenchmarkGenerativeTextArgs.model_fields[field] + if field_info.default_factory is not None: + return field_info.default_factory() + + return field_info.default + + model_config = ConfigDict( + extra="ignore", + use_enum_values=True, + from_attributes=True, + arbitrary_types_allowed=True, + ) + + # Required + target: str = Field(description="Target endpoint URL for benchmark execution") + data: list[Any] = Field( + description="List of dataset sources or data files", + default_factory=list, + min_length=1, + ) + # Benchmark configuration + profile: StrategyType | ProfileType | Profile = Field( + default="sweep", description="Benchmark profile or scheduling strategy type" + ) + rate: float | list[float] | None = Field( + default=None, description="Request rate(s) for rate-based scheduling" + ) + # Backend configuration + backend: BackendType | Backend = Field( + default="openai_http", description="Backend type or instance for execution" + ) + backend_kwargs: dict[str, Any] | None = Field( + default=None, description="Additional backend configuration arguments" + ) + model: str | None = Field(default=None, description="Model identifier for backend") + # Data configuration + processor: str | Path | PreTrainedTokenizerBase | None = Field( + default=None, description="Tokenizer path, name, or instance for processing" + ) + processor_args: dict[str, Any] | None = Field( + default=None, description="Additional tokenizer configuration arguments" + ) + data_args: list[dict[str, Any]] | None = Field( + default_factory=list, description="Per-dataset configuration arguments" + ) + data_samples: int = Field( + default=-1, description="Number of samples to use from datasets (-1 for all)" + ) + data_column_mapper: ( + DatasetPreprocessor | dict[str, str] | Literal["generative_column_mapper"] + ) = Field( + default="generative_column_mapper", + description="Column mapping preprocessor for dataset fields", + ) + data_request_formatter: DatasetPreprocessor | dict[str, str] | str = Field( + default="chat_completions", + description="Request formatting preprocessor or template name", + ) + data_collator: Callable | Literal["generative"] | None = Field( + default="generative", description="Data collator for batch processing" + ) + data_sampler: Sampler[int] | Literal["shuffle"] | None = Field( + default=None, description="Data sampler for request ordering" + ) + data_num_workers: int | None = Field( + default=None, description="Number of workers for data loading" + ) + dataloader_kwargs: dict[str, Any] | None = Field( + default=None, description="Additional dataloader configuration arguments" + ) + random_seed: int = Field(default=42, description="Random seed for reproducibility") + # Output configuration + output_path: str | Path | None = Field( + default_factory=Path.cwd, description="Directory path for output files" + ) + output_formats: list[str] | dict[str, str | dict[str, Any]] | None = Field( + default_factory=lambda: ["console", "json"], + description="Output format names or configuration mappings", + ) + # Benchmarker configuration + benchmark_cls: type[GenerativeBenchmark] = Field( + default=GenerativeBenchmark, + description="Benchmark class to use for result compilation", + ) + sample_requests: int | None = Field( + default=10, + description="Number of requests to sample for detailed metrics (None for all)", + ) + warmup: float | None = Field( + default=None, + description="Warmup period in seconds, requests, or fraction (0-1)", + ) + cooldown: float | None = Field( + default=None, + description="Cooldown period in seconds, requests, or fraction (0-1)", + ) + prefer_response_metrics: bool = Field( + default=True, + description="Whether to prefer backend response metrics over request metrics", + ) + # Constraints configuration + max_seconds: int | float | None = Field( + default=None, description="Maximum benchmark execution time in seconds" + ) + max_requests: int | None = Field( + default=None, description="Maximum number of requests to execute" + ) + max_errors: int | None = Field( + default=None, description="Maximum number of errors before stopping" + ) + max_error_rate: float | None = Field( + default=None, description="Maximum error rate (0-1) before stopping" + ) + max_global_error_rate: float | None = Field( + default=None, description="Maximum global error rate (0-1) before stopping" + ) + + @model_serializer + def serialize_model(self): + """ + Custom serialization logic for benchmark args. + + Converts complex types to serializable formats including Profile to type + string, Backend to type string, and Path objects to strings. + + :return: Dictionary representation suitable for JSON/YAML serialization + """ + return { + # target - serialize as is + "target": self.target, + "data": [ + item if isinstance(item, str | type(None)) else str(item) + for item in self.data + ], # data - for each item in the list, if not a str or None, save str(item) + "profile": ( + self.profile.type_ + if isinstance(self.profile, Profile) + else self.profile + ), # profile - if instance of Profile, then save as profile.type_ + "rate": self.rate, + "backend": ( + self.backend.type_ + if isinstance(self.backend, Backend) + else self.backend + ), # backend - if instance of Backend, then save as backend.type_ + "backend_kwargs": self.backend_kwargs, + "model": self.model, + "processor": ( + self.processor + if isinstance(self.processor, str) + else str(self.processor) + if self.processor is not None + else None + ), # processor - if not str, then save as str(processor) + "processor_args": self.processor_args, + "data_args": self.data_args, + "data_samples": self.data_samples, + "data_column_mapper": ( + self.data_column_mapper + if isinstance(self.data_column_mapper, dict | str) + else {} + ), # data_column_mapper - if not dict or str, then save as an empty dict + "data_request_formatter": ( + self.data_request_formatter + if isinstance(self.data_request_formatter, dict | str) + else {} + ), # data_request_formatter - if not dict or str, then save as empty dict + "data_collator": ( + self.data_collator if isinstance(self.data_collator, str) else None + ), # data_collator - if not str, then save as None + "data_sampler": ( + self.data_sampler if isinstance(self.data_sampler, str) else None + ), # data_sampler - if not str, then save as None + "data_num_workers": self.data_num_workers, + "dataloader_kwargs": self.dataloader_kwargs, + "random_seed": self.random_seed, + "output_path": ( + str(self.output_path) if self.output_path is not None else None + ), # output_path - if not None, then ensure it's a str + "output_formats": self.output_formats, + # benchmark_cls - don't save at all (excluded) + "sample_requests": self.sample_requests, + "warmup": self.warmup, + "cooldown": self.cooldown, + "prefer_response_metrics": self.prefer_response_metrics, + "max_seconds": self.max_seconds, + "max_requests": self.max_requests, + "max_errors": self.max_errors, + "max_error_rate": self.max_error_rate, + "max_global_error_rate": self.max_global_error_rate, + } + + +class GenerativeBenchmarksReport(StandardBaseModel): + """Container for multiple benchmark results with load/save functionality.""" + + DEFAULT_FILE: ClassVar[str] = "benchmarks.json" + + @staticmethod + def load_file( + path: str | Path, type_: Literal["json", "yaml"] | None = None + ) -> GenerativeBenchmarksReport: + """ + Load a report from a file. + + :param path: The path to load the report from. + :param type_: File type override, auto-detected from extension if None. + :return: The loaded report. + :raises ValueError: If file type is unsupported. + """ + path = Path(path) if not isinstance(path, Path) else path + + if path.is_dir(): + path = path / GenerativeBenchmarksReport.DEFAULT_FILE + + path.parent.mkdir(parents=True, exist_ok=True) + path_suffix = path.suffix.lower()[1:] + + with path.open("r") as file: + if (type_ or path_suffix) == "json": + model_dict = json.loads(file.read()) + elif (type_ or path_suffix) in ["yaml", "yml"]: + model_dict = yaml.safe_load(file) + else: + raise ValueError(f"Unsupported file type: {type_} for {path}.") + + return GenerativeBenchmarksReport.model_validate(model_dict) + + args: BenchmarkGenerativeTextArgs = Field( + description="The benchmark arguments used for all benchmarks in the report." + ) + benchmarks: list[GenerativeBenchmark] = Field( + description="The list of completed benchmarks contained within the report.", + default_factory=list, + ) + + def save_file( + self, path: str | Path | None, type_: Literal["json", "yaml"] | None = None + ) -> Path: + """ + Save the report to a file. + + :param path: The path to save the report to. + :param type_: File type override, auto-detected from extension if None. + :return: The path to the saved report. + :raises ValueError: If file type is unsupported. + """ + if path is None: + path = Path.cwd() + elif not isinstance(path, Path): + path = Path(path) + + if path.is_dir(): + path = path / GenerativeBenchmarksReport.DEFAULT_FILE + + path.parent.mkdir(parents=True, exist_ok=True) + path_suffix = path.suffix.lower()[1:] + model_dict = self.model_dump() + + if (type_ or path_suffix) == "json": + save_str = json.dumps(model_dict) + elif (type_ or path_suffix) in ["yaml", "yml"]: + save_str = yaml.dump(model_dict) + else: + raise ValueError(f"Unsupported file type: {type_} for {path}.") + + with path.open("w") as file: + file.write(save_str) + + return path diff --git a/src/guidellm/benchmark/types.py b/src/guidellm/benchmark/types.py deleted file mode 100644 index 1ef65a68..00000000 --- a/src/guidellm/benchmark/types.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -from collections.abc import Iterable -from pathlib import Path -from typing import Any - -from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict -from transformers import ( # type: ignore[import] - PreTrainedTokenizerBase, -) -from typing_extensions import TypeAliasType - -from guidellm.benchmark.aggregator import ( - Aggregator, - CompilableAggregator, -) -from guidellm.benchmark.output import ( - GenerativeBenchmarkerOutput, -) -from guidellm.benchmark.progress import BenchmarkerProgress - -__all__ = [ - "AggregatorInputT", - "DataInputT", - "OutputFormatT", - "ProcessorInputT", - "ProgressInputT", -] - - -DataInputT = TypeAliasType( - "DataInputT", - Iterable[str] - | Iterable[dict[str, Any]] - | Dataset - | DatasetDict - | IterableDataset - | IterableDatasetDict - | str - | Path, -) - -OutputFormatT = TypeAliasType( - "OutputFormatT", - tuple[str, ...] - | list[str] - | dict[str, str | dict[str, Any] | GenerativeBenchmarkerOutput] - | None, -) - -ProcessorInputT = TypeAliasType("ProcessorInputT", str | Path | PreTrainedTokenizerBase) - -ProgressInputT = TypeAliasType( - "ProgressInputT", tuple[str, ...] | list[str] | list[BenchmarkerProgress] -) - -AggregatorInputT = TypeAliasType( - "AggregatorInputT", - dict[str, str | dict[str, Any] | Aggregator | CompilableAggregator], -) diff --git a/src/guidellm/data/__init__.py b/src/guidellm/data/__init__.py index 8a48204e..0bff1b64 100644 --- a/src/guidellm/data/__init__.py +++ b/src/guidellm/data/__init__.py @@ -1,4 +1,28 @@ -""" -Required for python < 3.12 -https://docs.python.org/3/library/importlib.resources.html#importlib.resources.files -""" +from .collators import GenerativeRequestCollator +from .deserializers import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) +from .loaders import DataLoader, DatasetsIterator +from .preprocessors import ( + DataDependentPreprocessor, + DatasetPreprocessor, + PreprocessorRegistry, +) +from .processor import ProcessorFactory +from .schemas import GenerativeDatasetColumnType + +__all__ = [ + "DataDependentPreprocessor", + "DataLoader", + "DataNotSupportedError", + "DatasetDeserializer", + "DatasetDeserializerFactory", + "DatasetPreprocessor", + "DatasetsIterator", + "GenerativeDatasetColumnType", + "GenerativeRequestCollator", + "PreprocessorRegistry", + "ProcessorFactory", +] diff --git a/src/guidellm/data/collators.py b/src/guidellm/data/collators.py new file mode 100644 index 00000000..f9e1ade4 --- /dev/null +++ b/src/guidellm/data/collators.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from guidellm.schemas import GenerationRequest + +__all__ = ["GenerativeRequestCollator"] + + +class GenerativeRequestCollator: + def __call__(self, batch: list) -> GenerationRequest: + if len(batch) != 1: + raise NotImplementedError( + f"Batch size greater than 1 is not currently supported. " + f"Got batch size: {len(batch)}" + ) + + return batch[0] diff --git a/src/guidellm/data/deserializers/__init__.py b/src/guidellm/data/deserializers/__init__.py new file mode 100644 index 00000000..1062f2b7 --- /dev/null +++ b/src/guidellm/data/deserializers/__init__.py @@ -0,0 +1,53 @@ +from .deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) +from .file import ( + ArrowFileDatasetDeserializer, + CSVFileDatasetDeserializer, + DBFileDatasetDeserializer, + HDF5FileDatasetDeserializer, + JSONFileDatasetDeserializer, + ParquetFileDatasetDeserializer, + TarFileDatasetDeserializer, + TextFileDatasetDeserializer, +) +from .huggingface import HuggingFaceDatasetDeserializer +from .memory import ( + InMemoryCsvDatasetDeserializer, + InMemoryDictDatasetDeserializer, + InMemoryDictListDatasetDeserializer, + InMemoryItemListDatasetDeserializer, + InMemoryJsonStrDatasetDeserializer, +) +from .synthetic import ( + SyntheticTextDatasetConfig, + SyntheticTextDatasetDeserializer, + SyntheticTextGenerator, + SyntheticTextPrefixBucketConfig, +) + +__all__ = [ + "ArrowFileDatasetDeserializer", + "CSVFileDatasetDeserializer", + "DBFileDatasetDeserializer", + "DataNotSupportedError", + "DatasetDeserializer", + "DatasetDeserializerFactory", + "HDF5FileDatasetDeserializer", + "HuggingFaceDatasetDeserializer", + "InMemoryCsvDatasetDeserializer", + "InMemoryDictDatasetDeserializer", + "InMemoryDictListDatasetDeserializer", + "InMemoryItemListDatasetDeserializer", + "InMemoryJsonStrDatasetDeserializer", + "JSONFileDatasetDeserializer", + "ParquetFileDatasetDeserializer", + "SyntheticTextDatasetConfig", + "SyntheticTextDatasetDeserializer", + "SyntheticTextGenerator", + "SyntheticTextPrefixBucketConfig", + "TarFileDatasetDeserializer", + "TextFileDatasetDeserializer", +] diff --git a/src/guidellm/data/deserializers/deserializer.py b/src/guidellm/data/deserializers/deserializer.py new file mode 100644 index 00000000..7f0dae39 --- /dev/null +++ b/src/guidellm/data/deserializers/deserializer.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import contextlib +from collections.abc import Callable +from typing import Any, Protocol, Union, runtime_checkable + +from datasets import Dataset, IterableDataset +from transformers import PreTrainedTokenizerBase + +from guidellm.data.utils import resolve_dataset_split +from guidellm.utils import RegistryMixin + +__all__ = [ + "DataNotSupportedError", + "DatasetDeserializer", + "DatasetDeserializerFactory", +] + + +class DataNotSupportedError(Exception): + """Exception raised when data format is not supported by deserializer.""" + + +@runtime_checkable +class DatasetDeserializer(Protocol): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: ... + + +class DatasetDeserializerFactory( + RegistryMixin[Union["type[DatasetDeserializer]", DatasetDeserializer]], +): + @classmethod + def deserialize( + cls, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int = 42, + type_: str | None = None, + resolve_split: bool = True, + select_columns: list[str] | None = None, + remove_columns: list[str] | None = None, + **data_kwargs: dict[str, Any], + ) -> Dataset | IterableDataset: + dataset = None + + if type_ is None: + for name, deserializer in cls.registry.items(): + if name == "huggingface": + # Save Hugging Face til the end since it is a catch-all. + continue + + deserializer_fn: DatasetDeserializer = ( + deserializer() if isinstance(deserializer, type) else deserializer + ) + + with contextlib.suppress(DataNotSupportedError): + dataset = deserializer_fn( + data=data, + processor_factory=processor_factory, + random_seed=random_seed, + **data_kwargs, + ) + + if dataset is None: + deserializer_fn = cls.get_registered_object("huggingface")() + dataset = deserializer_fn( + data=data, + processor_factory=processor_factory, + random_seed=random_seed, + **data_kwargs, + ) + elif deserializer := cls.get_registered_object(type_) is not None: + deserializer_fn: DatasetDeserializer = ( + deserializer() if isinstance(deserializer, type) else deserializer + ) + + dataset = deserializer_fn( + data=data, + processor_factory=processor_factory, + random_seed=random_seed, + **data_kwargs, + ) + + if dataset is None: + raise DataNotSupportedError( + f"No suitable deserializer found for data {data} " + f"with kwargs {data_kwargs} and deserializer type {type_}." + ) + + if resolve_split: + dataset = resolve_dataset_split(dataset) + + if select_columns is not None or remove_columns is not None: + column_names = dataset.column_names or list(next(iter(dataset)).keys()) + if select_columns is not None: + remove_columns = [ + col for col in column_names if col not in select_columns + ] + + dataset = dataset.remove_columns(remove_columns) + + return dataset diff --git a/src/guidellm/data/deserializers/file.py b/src/guidellm/data/deserializers/file.py new file mode 100644 index 00000000..d57403db --- /dev/null +++ b/src/guidellm/data/deserializers/file.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import pandas as pd +from datasets import Dataset, load_dataset +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers.deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) + +__all__ = [ + "ArrowFileDatasetDeserializer", + "CSVFileDatasetDeserializer", + "DBFileDatasetDeserializer", + "HDF5FileDatasetDeserializer", + "JSONFileDatasetDeserializer", + "ParquetFileDatasetDeserializer", + "TarFileDatasetDeserializer", + "TextFileDatasetDeserializer", +] + + +@DatasetDeserializerFactory.register("text_file") +class TextFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) # Ignore unused args format errors + + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() not in {".txt", ".text"} + ): + raise DataNotSupportedError( + "Unsupported data for TextFileDatasetDeserializer, " + f"expected str or Path to a local .txt or .text file, got {data}" + ) + + with path.open() as file: + lines = file.readlines() + + return Dataset.from_dict({"text": lines}, **data_kwargs) + + +@DatasetDeserializerFactory.register("csv_file") +class CSVFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".csv" + ): + raise DataNotSupportedError( + "Unsupported data for CSVFileDatasetDeserializer, " + f"expected str or Path to a local .csv file, got {data}" + ) + + return load_dataset("csv", data_files=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("json_file") +class JSONFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() not in {".json", ".jsonl"} + ): + raise DataNotSupportedError( + f"Unsupported data for JSONFileDatasetDeserializer, " + f"expected str or Path to a local .json or .jsonl file, got {data}" + ) + + return load_dataset("json", data_files=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("parquet_file") +class ParquetFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".parquet" + ): + raise DataNotSupportedError( + f"Unsupported data for ParquetFileDatasetDeserializer, " + f"expected str or Path to a local .parquet file, got {data}" + ) + + return load_dataset("parquet", data_files=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("arrow_file") +class ArrowFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".arrow" + ): + raise DataNotSupportedError( + f"Unsupported data for ArrowFileDatasetDeserializer, " + f"expected str or Path to a local .arrow file, got {data}" + ) + + return load_dataset("arrow", data_files=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("hdf5_file") +class HDF5FileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() not in {".hdf5", ".h5"} + ): + raise DataNotSupportedError( + f"Unsupported data for HDF5FileDatasetDeserializer, " + f"expected str or Path to a local .hdf5 or .h5 file, got {data}" + ) + + return Dataset.from_pandas(pd.read_hdf(str(path)), **data_kwargs) + + +@DatasetDeserializerFactory.register("db_file") +class DBFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".db" + ): + raise DataNotSupportedError( + f"Unsupported data for DBFileDatasetDeserializer, " + f"expected str or Path to a local .db file, got {data}" + ) + + return Dataset.from_sql(con=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("tar_file") +class TarFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".tar" + ): + raise DataNotSupportedError( + f"Unsupported data for TarFileDatasetDeserializer, " + f"expected str or Path to a local .tar file, got {data}" + ) + + return load_dataset("webdataset", data_files=str(path), **data_kwargs) diff --git a/src/guidellm/data/deserializers/huggingface.py b/src/guidellm/data/deserializers/huggingface.py new file mode 100644 index 00000000..80e0ed8c --- /dev/null +++ b/src/guidellm/data/deserializers/huggingface.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from datasets import ( + Dataset, + DatasetDict, + IterableDataset, + IterableDatasetDict, + load_dataset, + load_from_disk, +) +from datasets.exceptions import ( + DataFilesNotFoundError, + DatasetNotFoundError, + FileNotFoundDatasetsError, +) +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers.deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) + +__all__ = ["HuggingFaceDatasetDeserializer"] + + +@DatasetDeserializerFactory.register("huggingface") +class HuggingFaceDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + + if isinstance( + data, Dataset | IterableDataset | DatasetDict | IterableDatasetDict + ): + return data + + load_error = None + + if ( + isinstance(data, str | Path) + and (path := Path(data)).exists() + and ((path.is_file() and path.suffix == ".py") or path.is_dir()) + ): + # Handle python script or nested python script in a directory + try: + return load_dataset(str(data), **data_kwargs) + except ( + FileNotFoundDatasetsError, + DatasetNotFoundError, + DataFilesNotFoundError, + ) as err: + load_error = err + except Exception: # noqa: BLE001 + # Try loading as a local dataset directory next + try: + return load_from_disk(str(data), **data_kwargs) + except ( + FileNotFoundDatasetsError, + DatasetNotFoundError, + DataFilesNotFoundError, + ) as err2: + load_error = err2 + + try: + # Handle dataset identifier from the Hugging Face Hub + return load_dataset(str(data), **data_kwargs) + except ( + FileNotFoundDatasetsError, + DatasetNotFoundError, + DataFilesNotFoundError, + ) as err: + load_error = err + + not_supported = DataNotSupportedError( + "Unsupported data for HuggingFaceDatasetDeserializer, " + "expected Dataset, IterableDataset, DatasetDict, IterableDatasetDict, " + "str or Path to a local dataset directory or a local .py dataset script, " + f"got {data} and HF load error: {load_error}" + ) + + if load_error is not None: + raise not_supported from load_error + else: + raise not_supported diff --git a/src/guidellm/data/deserializers/memory.py b/src/guidellm/data/deserializers/memory.py new file mode 100644 index 00000000..6f8888ec --- /dev/null +++ b/src/guidellm/data/deserializers/memory.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +import contextlib +import csv +import json +from collections.abc import Callable +from io import StringIO +from typing import Any, cast + +from datasets import Dataset +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers.deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) + +__all__ = [ + "InMemoryCsvDatasetDeserializer", + "InMemoryDictDatasetDeserializer", + "InMemoryDictListDatasetDeserializer", + "InMemoryItemListDatasetDeserializer", + "InMemoryJsonStrDatasetDeserializer", +] + + +@DatasetDeserializerFactory.register("in_memory_dict") +class InMemoryDictDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) # Ignore unused args format errors + + if ( + not data + or not isinstance(data, dict) + or not all( + isinstance(key, str) and isinstance(val, list) + for key, val in data.items() + ) + ): + raise DataNotSupportedError( + f"Unsupported data for InMemoryDictDatasetDeserializer, " + f"expected dict[str, list], got {data}" + ) + + rows = len(list(data.values())[0]) + if not all(len(val) == rows for val in data.values()): + raise DataNotSupportedError( + "All lists in the data dictionary must have the same length, " + f"expected {rows} for all keys {list(data.keys())}" + ) + + return Dataset.from_dict(data, **data_kwargs) + + +@DatasetDeserializerFactory.register("in_memory_dict_list") +class InMemoryDictListDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) # Ignore unused args format errors + + if ( + not data + or not isinstance(data, list) + or not all(isinstance(item, dict) for item in data) + or not all(isinstance(key, str) for item in data for key in item) + ): + raise DataNotSupportedError( + f"Unsupported data for InMemoryDictListDatasetDeserializer, " + f"expected list of dicts, got {data}" + ) + + data: list[dict[str, Any]] = cast("list[dict[str, Any]]", data) + first_keys = set(data[0].keys()) + for index, item in enumerate(data): + if set(item.keys()) != first_keys: + raise DataNotSupportedError( + f"All dictionaries must have the same keys. " + f"Expected keys: {first_keys}, " + f"got keys at index {index}: {set(item.keys())}" + ) + + # Convert list of dicts to dict of lists + result_dict = {key: [] for key in first_keys} + for item in data: + for key, value in item.items(): + result_dict[key].append(value) + + return Dataset.from_dict(result_dict, **data_kwargs) + + +@DatasetDeserializerFactory.register("in_memory_item_list") +class InMemoryItemListDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + _ = (processor_factory, random_seed) # Ignore unused args format errors + + primitive_types = (str, int, float, bool, type(None)) + if ( + not data + or not isinstance(data, list) + or not all(isinstance(item, primitive_types) for item in data) + ): + raise DataNotSupportedError( + f"Unsupported data for InMemoryItemListDatasetDeserializer, " + f"expected list of primitive items, got {data}" + ) + + column_name = data_kwargs.pop("column_name", "data") + + return Dataset.from_dict({column_name: data}, **data_kwargs) + + +@DatasetDeserializerFactory.register("in_memory_json_str") +class InMemoryJsonStrDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + if ( + isinstance(data, str) + and (json_str := data.strip()) + and ( + (json_str.startswith("{") and json_str.endswith("}")) + or (json_str.startswith("[") and json_str.endswith("]")) + ) + ): + with contextlib.suppress(Exception): + parsed = json.loads(data) + + for deserializer in [ + InMemoryDictDatasetDeserializer, + InMemoryDictListDatasetDeserializer, + InMemoryItemListDatasetDeserializer, + ]: + with contextlib.suppress(DataNotSupportedError): + return deserializer()( + parsed, data_kwargs, processor_factory, random_seed + ) + + raise DataNotSupportedError( + f"Unsupported data for InMemoryJsonStrDatasetDeserializer, " + f"expected JSON string with a list or dict of items, got {data}" + ) + + +@DatasetDeserializerFactory.register("in_memory_csv_str") +class InMemoryCsvDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> dict[str, list]: + if ( + isinstance(data, str) + and (csv_str := data.strip()) + and len(csv_str.split("\n")) > 0 + ): + with contextlib.suppress(Exception): + csv_buffer = StringIO(data) + reader = csv.DictReader(csv_buffer) + rows = list(reader) + + return InMemoryDictListDatasetDeserializer()( + rows, processor_factory, random_seed, **data_kwargs + ) + + raise DataNotSupportedError( + f"Unsupported data for InMemoryCsvDatasetDeserializer, " + f"expected CSV string, got {type(data)}" + ) diff --git a/src/guidellm/data/deserializers/synthetic.py b/src/guidellm/data/deserializers/synthetic.py new file mode 100644 index 00000000..fcd9b08a --- /dev/null +++ b/src/guidellm/data/deserializers/synthetic.py @@ -0,0 +1,385 @@ +from __future__ import annotations + +import math +from collections.abc import Callable, Iterator +from pathlib import Path +from random import Random +from typing import Any + +import yaml +from datasets import Features, IterableDataset, List, Value +from faker import Faker +from pydantic import ConfigDict, Field, model_validator +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers.deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) +from guidellm.utils import IntegerRangeSampler, StandardBaseModel + +__all__ = [ + "SyntheticTextDatasetConfig", + "SyntheticTextDatasetDeserializer", + "SyntheticTextGenerator", + "SyntheticTextPrefixBucketConfig", +] + + +class SyntheticTextPrefixBucketConfig(StandardBaseModel): + bucket_weight: int = Field( + description="Weight of this bucket in the overall distribution.", + gt=0, + default=100, + ) + prefix_count: int = Field( + description="The number of unique prefixes to generate for this bucket.", + ge=1, + default=1, + ) + prefix_tokens: int = Field( + description="The number of prefix tokens per-prompt for this bucket.", + ge=0, + default=0, + ) + + +class SyntheticTextDatasetConfig(StandardBaseModel): + model_config = ConfigDict( + extra="allow", + ) + + prefix_buckets: list[SyntheticTextPrefixBucketConfig] | None = Field( + description="Buckets for the prefix tokens distribution.", + default=None, + ) + prompt_tokens: int = Field( + description="The average number of text tokens generated for prompts.", + gt=0, + ) + prompt_tokens_stdev: int | None = Field( + description="The standard deviation of the tokens generated for prompts.", + gt=0, + default=None, + ) + prompt_tokens_min: int | None = Field( + description="The minimum number of text tokens generated for prompts.", + gt=0, + default=None, + ) + prompt_tokens_max: int | None = Field( + description="The maximum number of text tokens generated for prompts.", + gt=0, + default=None, + ) + output_tokens: int = Field( + description="The average number of text tokens generated for outputs.", + gt=0, + ) + output_tokens_stdev: int | None = Field( + description="The standard deviation of the tokens generated for outputs.", + gt=0, + default=None, + ) + output_tokens_min: int | None = Field( + description="The minimum number of text tokens generated for outputs.", + gt=0, + default=None, + ) + output_tokens_max: int | None = Field( + description="The maximum number of text tokens generated for outputs.", + gt=0, + default=None, + ) + turns: int = Field( + description="The number of turns in the conversation.", + gt=0, + default=1, + ) + turns_stdev: int | None = Field( + description="The standard deviation of the number of turns.", + gt=0, + default=None, + ) + turns_min: int | None = Field( + description="The minimum number of turns in the conversation.", + gt=0, + default=None, + ) + turns_max: int | None = Field( + description="The maximum number of turns in the conversation.", + gt=0, + default=None, + ) + source: str = Field( + description="The source of the text data to be used for generation.", + default="data:prideandprejudice.txt.gz", + ) + + @model_validator(mode="after") + def check_prefix_options(self) -> SyntheticTextDatasetConfig: + prefix_count = self.__pydantic_extra__.get("prefix_count", None) # type: ignore[attr-defined] + prefix_tokens = self.__pydantic_extra__.get("prefix_tokens", None) # type: ignore[attr-defined] + if prefix_count is not None or prefix_tokens is not None: + if self.prefix_buckets: + raise ValueError( + "prefix_buckets is mutually exclusive" + " with prefix_count and prefix_tokens" + ) + + self.prefix_buckets = [ + SyntheticTextPrefixBucketConfig( + prefix_count=prefix_count or 1, + prefix_tokens=prefix_tokens or 0, + ) + ] + + return self + + +class SyntheticTextGenerator: + def __init__( + self, + config: SyntheticTextDatasetConfig, + processor: PreTrainedTokenizerBase, + random_seed: int = 42, + ): + self.config = config + self.processor = processor + self.random_seed = random_seed + + def __iter__(self) -> Iterator[dict[str, Any]]: + samples_generated = 0 + + faker = Faker() + faker.seed_instance(self.random_seed) + prompt_tokens_sampler = iter( + IntegerRangeSampler( + average=self.config.prompt_tokens, + variance=self.config.prompt_tokens_stdev, + min_value=self.config.prompt_tokens_min, + max_value=self.config.prompt_tokens_max, + random_seed=self.random_seed, + ) + ) + output_tokens_sampler = iter( + IntegerRangeSampler( + average=self.config.output_tokens, + variance=self.config.output_tokens_stdev, + min_value=self.config.output_tokens_min, + max_value=self.config.output_tokens_max, + random_seed=self.random_seed + 1, # ensure diff dist from prompts + ) + ) + turns_sampler = iter( + IntegerRangeSampler( + average=self.config.turns, + variance=self.config.turns_stdev, + min_value=self.config.turns_min, + max_value=self.config.turns_max, + random_seed=self.random_seed + 5, # ensure diff dist + ) + ) + + # Create a shared prefix if specified + rand = Random(self.random_seed + 3) + prefix_iter = self._create_prefix_iter(faker, rand) + + while True: + prompt_tokens_counts = [] + output_tokens_counts = [] + prompts = [] + + # Iterate over each turn + turns = next(turns_sampler) + for _ in range(turns): + prompt_tokens_counts.append(next(prompt_tokens_sampler)) + output_tokens_counts.append(next(output_tokens_sampler)) + prompts.append( + self._create_prompt( + prompt_tokens_counts[-1], faker, f"{samples_generated} " + ) + ) + samples_generated += 1 + + yield { + "prefix": next(prefix_iter), + "prompt": prompts, + "prompt_tokens_count": prompt_tokens_counts, + "output_tokens_count": output_tokens_counts, + } + + def _create_prompt( + self, prompt_tokens_count: int, faker: Faker, unique: str = "" + ) -> str: + prompt_token_ids = [] + avg_chars_per_token = 5 + margin_of_safety = 1.5 + attempts = 0 + + while len(prompt_token_ids) < prompt_tokens_count: + attempts += 1 + num_chars = ( + prompt_tokens_count * avg_chars_per_token * margin_of_safety * attempts + ) + text = unique + faker.text(max_nb_chars=num_chars) + prompt_token_ids = self.processor.encode(text) + + return self.processor.decode( + prompt_token_ids[:prompt_tokens_count], skip_special_tokens=True + ) + + def _create_prefix_iter(self, faker: Faker, rand: Random) -> Iterator[str]: + if not self.config.prefix_buckets: + while True: + yield "" + + # Increase weights to ensure an integer number of samples per per-prefix + least_common_prefix_count = math.lcm( + *(bucket.prefix_count for bucket in self.config.prefix_buckets) + ) + unnorm_weights = [ + least_common_prefix_count * bucket.bucket_weight // bucket.prefix_count + for bucket in self.config.prefix_buckets + ] + # Use GCD to reduce the weights to smallest integer ratio + common_divisor = math.gcd(*unnorm_weights) + + # Create prefix list maintaining the correct distribution + prefixes = [] + for bucket, weight in zip( + self.config.prefix_buckets, unnorm_weights, strict=False + ): + bucket_prefixes = [ + self._create_prompt(bucket.prefix_tokens, faker) + for _ in range(bucket.prefix_count) + ] + sample_count = weight // common_divisor + prefixes.extend(bucket_prefixes * sample_count) + + while True: + yield rand.choice(prefixes) + + +@DatasetDeserializerFactory.register("synthetic_text") +class SyntheticTextDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + **data_kwargs: dict[str, Any], + ) -> IterableDataset: + # Config file pathways, deserialize and call self again + if (config := self._load_config_file(data)) is not None: + return self(config, processor_factory, random_seed, **data_kwargs) + + # Config str pathways, deserialize and call self again + if (config := self._load_config_str(data)) is not None: + return self(config, processor_factory, random_seed, **data_kwargs) + + if not isinstance(data, SyntheticTextDatasetConfig): + raise DataNotSupportedError( + "Unsupported data for SyntheticTextDatasetDeserializer, " + "expected SyntheticTextDatasetConfig, str or Path to a config file, " + f"got {data}" + ) + + return IterableDataset.from_generator( + SyntheticTextGenerator, + gen_kwargs={ + "config": data, + "processor": processor_factory(), + "random_seed": random_seed, + }, + features=Features( + { + "prefix": Value("string"), + "prompt": List(Value("string")), + "prompt_tokens_count": List(Value("int32")), + "output_tokens_count": List(Value("int32")), + } + ), + ) + + def _load_config_file(self, data: Any) -> SyntheticTextDatasetConfig | None: + if (not isinstance(data, str) and not isinstance(data, Path)) or ( + not Path(data).is_file() + ): + return None + + data_path = Path(data) if isinstance(data, str) else data + error = None + + if Path(data).is_file() and data_path.suffix.lower() == ".json": + try: + return SyntheticTextDatasetConfig.model_validate_json( + data_path.read_text() + ) + except Exception as err: # noqa: BLE001 + error = err + + if Path(data).is_file() and data_path.suffix.lower() in { + ".yaml", + ".yml", + ".config", + }: + try: + return SyntheticTextDatasetConfig.model_validate( + yaml.safe_load(data_path.read_text()) + ) + except Exception as err: # noqa: BLE001 + error = err + + err_message = ( + f"Unsupported file {data_path} for " + f"SyntheticTextDatasetDeserializer, expected .json, " + f".yaml, .yml, or .config" + ) + + if error is not None: + err_message += f" with error: {error}" + raise DataNotSupportedError(err_message) from error + raise DataNotSupportedError(err_message) + + def _load_config_str(self, data: str) -> SyntheticTextDatasetConfig | None: + if not isinstance(data, str): + return None + + data_str = data.strip() + error = None + + if (data_str.startswith("{") and data_str.endswith("}")) or ( + data_str.startswith("[") and data_str.endswith("]") + ): + try: + return SyntheticTextDatasetConfig.model_validate_json(data_str) + except Exception as err: # noqa: BLE001 + error = err + + if data_str.count("=") > 1: + # key=value pairs separated by commas + try: + config_dict = {} + items = data_str.split(",") + for item in items: + key, value = item.split("=") + config_dict[key.strip()] = ( + int(value.strip()) + if value.strip().isnumeric() + else value.strip() + ) + + return SyntheticTextDatasetConfig.model_validate(config_dict) + except Exception as err: # noqa: BLE001 + error = err + + err_message = ( + "Unsupported string data for SyntheticTextDatasetDeserializer, " + f"expected JSON or key-value pairs, got {data}" + ) + if error is not None: + err_message += f" with error: {error}" + raise DataNotSupportedError(err_message) from error + raise DataNotSupportedError(err_message) diff --git a/src/guidellm/data/loaders.py b/src/guidellm/data/loaders.py new file mode 100644 index 00000000..fd46334d --- /dev/null +++ b/src/guidellm/data/loaders.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +import contextlib +from collections.abc import Callable, Iterator +from typing import Any, Literal + +import torch +from torch.utils.data import Sampler +from torch.utils.data.dataloader import DataLoader as PyTorchDataLoader +from torch.utils.data.dataset import IterableDataset as TorchIterableDataset +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers import DatasetDeserializerFactory +from guidellm.data.preprocessors import DataDependentPreprocessor, DatasetPreprocessor +from guidellm.logger import logger + +__all__ = ["DataLoader", "DatasetsIterator"] + + +class DatasetsIterator(TorchIterableDataset): + def __init__( + self, + data: list[Any], + data_args: list[dict[str, Any]] | None, + data_samples: int, + processor_factory: Callable[[], PreTrainedTokenizerBase], + preprocessors: list[DatasetPreprocessor | DataDependentPreprocessor], + random_seed: int, + ): + if not data or not isinstance(data, list): + raise ValueError(f"Data must be a non-empty list, got {data}.") + + if not data_args: + data_args = [{} for _ in data] + + if len(data) != len(data_args): + raise ValueError( + f"Length of data ({len(data)}) must match length of data_args " + f"({len(data_args)})." + ) + + self.datasets = [] + for datum, data_kwargs in zip(data, data_args, strict=False): + self.datasets.append( + DatasetDeserializerFactory.deserialize( + data=datum, + processor_factory=processor_factory, + random_seed=random_seed, + **data_kwargs, + ) + ) + self.preprocessors = preprocessors + for preprocessor in self.preprocessors: + if isinstance(preprocessor, DataDependentPreprocessor): + preprocessor.setup_data( + datasets=self.datasets, + data_args=data_args, + ) + self.precache: list[Any] | None = ( + list(self.generator(data_samples)) if data_samples else None + ) + + def __iter__(self): + worker_info = torch.utils.data.get_worker_info() + worker_modulus = worker_info.num_workers if worker_info is not None else 1 + worker_index = worker_info.id if worker_info is not None else 0 + + if self.precache: + for index, item in enumerate(self.precache): + if (index + worker_index) % worker_modulus == 0: + yield item + else: + yield from self.generator(modulus=worker_modulus, offset=worker_index) + + def generator( + self, + max_items: int | None = None, + modulus: int | None = None, + offset: int | None = None, + ) -> Iterator[Any]: + gen_count = 0 + + with contextlib.suppress(StopIteration): + dataset_iters = [iter(dataset) for dataset in self.datasets] + + while max_items is None or gen_count < max_items: + try: + row = { + "items": [next(dataset_iter) for dataset_iter in dataset_iters] + } + gen_count += 1 + + if ( + modulus is not None + and offset is not None + and (gen_count % modulus) != offset + ): + continue + + for preprocessor in self.preprocessors: + row = preprocessor(row) + yield row + except Exception as err: + logger.error(f"Skipping data row due to error: {err}") + gen_count -= 1 + + if max_items is not None and gen_count < max_items: + raise ValueError( + f"Requested {max_items} samples, but only {gen_count} " + "available from the provided datasets." + ) + + +class DataLoader(PyTorchDataLoader): + def __init__( + self, + data: list[Any], + data_args: list[dict[str, Any]] | None, + data_samples: int, + processor_factory: Callable[[], PreTrainedTokenizerBase], + preprocessors: list[DatasetPreprocessor | DataDependentPreprocessor], + collator: Callable, + sampler: Sampler[int] | Literal["shuffle"] | None = None, + num_workers: int | None = 1, + random_seed: int = 42, + **kwargs: Any, + ): + iterator = DatasetsIterator( + data=data, + data_args=data_args, + data_samples=data_samples, + processor_factory=processor_factory, + preprocessors=preprocessors, + random_seed=random_seed, + ) + + super().__init__( + dataset=iterator, + batch_size=1, + shuffle=sampler == "shuffle", + sampler=sampler if sampler != "shuffle" else None, + collate_fn=collator, + num_workers=num_workers or 0, + **kwargs, + ) diff --git a/src/guidellm/data/preprocessors/__init__.py b/src/guidellm/data/preprocessors/__init__.py new file mode 100644 index 00000000..664e196b --- /dev/null +++ b/src/guidellm/data/preprocessors/__init__.py @@ -0,0 +1,25 @@ +from .formatters import ( + GenerativeAudioTranscriptionRequestFormatter, + GenerativeAudioTranslationRequestFormatter, + GenerativeChatCompletionsRequestFormatter, + GenerativeTextCompletionsRequestFormatter, +) +from .mappers import GenerativeColumnMapper +from .preprocessor import ( + DataDependentPreprocessor, + DatasetPreprocessor, + PreprocessorRegistry, +) + +__all__ = [ + "ColumnMapper", + "ColumnMapperRegistry", + "DataDependentPreprocessor", + "DatasetPreprocessor", + "GenerativeAudioTranscriptionRequestFormatter", + "GenerativeAudioTranslationRequestFormatter", + "GenerativeChatCompletionsRequestFormatter", + "GenerativeColumnMapper", + "GenerativeTextCompletionsRequestFormatter", + "PreprocessorRegistry", +] diff --git a/src/guidellm/data/preprocessors/formatters.py b/src/guidellm/data/preprocessors/formatters.py new file mode 100644 index 00000000..a243ad8c --- /dev/null +++ b/src/guidellm/data/preprocessors/formatters.py @@ -0,0 +1,458 @@ +from __future__ import annotations + +from abc import ABCMeta +from typing import Any + +from guidellm.data.preprocessors.preprocessor import ( + DatasetPreprocessor, + PreprocessorRegistry, +) +from guidellm.data.schemas import GenerativeDatasetColumnType +from guidellm.data.utils import text_stats +from guidellm.schemas import GenerationRequest, GenerationRequestArguments, UsageMetrics + +__all__ = [ + "GenerativeAudioTranscriptionRequestFormatter", + "GenerativeAudioTranslationRequestFormatter", + "GenerativeChatCompletionsRequestFormatter", + "GenerativeTextCompletionsRequestFormatter", +] + + +class RequestFormatter(DatasetPreprocessor, metaclass=ABCMeta): + @staticmethod + def encode_audio(*args, **kwargs): + from guidellm.extras.audio import encode_audio + + return encode_audio(*args, **kwargs) + + @staticmethod + def encode_image(*args, **kwargs): + from guidellm.extras.vision import encode_image + + return encode_image(*args, **kwargs) + + @staticmethod + def encode_video(*args, **kwargs): + from guidellm.extras.vision import encode_video + + return encode_video(*args, **kwargs) + + +@PreprocessorRegistry.register("text_completions") +class GenerativeTextCompletionsRequestFormatter(RequestFormatter): + def __init__( + self, + model: str, + extras: dict[str, Any] | GenerationRequestArguments | None = None, + stream: bool = True, + max_tokens: int | None = None, + max_completion_tokens: int | None = None, + ): + self.model: str | None = model + self.extras = ( + GenerationRequestArguments(**extras) + if extras and isinstance(extras, dict) + else extras + ) + self.stream: bool = stream + self.max_tokens: int | None = max_tokens or max_completion_tokens + + def __call__( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest | list[GenerationRequest]: + text_col = columns.get("text_column", []) + if any(isinstance(col, list) for col in text_col): + if len(text_col) > 1: + raise ValueError( + "Multi-turn currently not supported with dataset concatenation." + ) + turns = [{} for _ in range(len(text_col[0]))] + for col_type, col_values in columns.items(): + if isinstance(col_values[0], list): + for turn_idx, turn_value in zip( + range(len(turns)), col_values[0], strict=False + ): + turns[turn_idx][col_type] = [turn_value] + else: + for turn_idx in range(len(turns)): + turns[turn_idx][col_type] = [col_values[0]] + return [self._apply(turn_columns) for turn_columns in turns] + else: + return self._apply(columns) + + def _apply( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest: + arguments: GenerationRequestArguments = GenerationRequestArguments(body={}) + input_metrics = UsageMetrics() + output_metrics = UsageMetrics() + + # Add model + if self.model is not None: + arguments.body["model"] = self.model + + # Configure streaming + if self.stream: + arguments.stream = True + arguments.body["stream"] = True + + # Handle output tokens + if output_tokens := sum( + count for count in columns.get("output_tokens_count_column", []) if count + ): + output_metrics.text_tokens = output_tokens + arguments.body["max_tokens"] = output_tokens + arguments.body["stop"] = None + arguments.body["ignore_eos"] = True + elif self.max_tokens is not None: + arguments.body["max_tokens"] = self.max_tokens + + # Handle prompt tokens + if prompt_tokens := sum( + count for count in columns.get("prompt_tokens_count_column", []) if count + ): + input_metrics.text_tokens = prompt_tokens + + # Apply extra arguments + if self.extras: + arguments.model_combine(self.extras) + + # Build prompt + prefix = "".join(pre for pre in columns.get("prefix_column", []) if pre) + text = "".join(txt for txt in columns.get("text_column", []) if txt) + if prefix or text: + arguments.body["prompt"] = prefix + text + stats = text_stats(arguments.body["prompt"]) + input_metrics.text_characters = stats.get("num_chars") + input_metrics.text_words = stats.get("num_words") + + return GenerationRequest( + request_type="text_completions", + arguments=arguments, + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + +@PreprocessorRegistry.register("chat_completions") +class GenerativeChatCompletionsRequestFormatter(RequestFormatter): + def __init__( + self, + model: str, + extras: dict[str, Any] | GenerationRequestArguments | None = None, + stream: bool = True, + max_tokens: int | None = None, + max_completion_tokens: int | None = None, + encode_kwargs: dict[str, Any] | None = None, + ): + self.model = model + self.extras = ( + GenerationRequestArguments(**extras) + if extras and isinstance(extras, dict) + else extras + ) + self.stream = stream + self.max_completion_tokens = max_tokens or max_completion_tokens + self.encode_image_kwargs = ( + encode_kwargs.get("image", {}) if encode_kwargs else {} + ) + self.encode_video_kwargs = ( + encode_kwargs.get("video", {}) if encode_kwargs else {} + ) + self.encode_audio_kwargs = ( + encode_kwargs.get("audio", {}) if encode_kwargs else {} + ) + + def __call__( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest | list[GenerationRequest]: + text_col = columns.get("text_column", []) + if any(isinstance(col, list) for col in text_col): + if len(text_col) > 1: + raise ValueError( + "Multi-turn currently not supported with dataset concatenation." + ) + turns = [{} for _ in range(len(text_col[0]))] + for col_type, col_values in columns.items(): + if isinstance(col_values[0], list): + for turn_idx, turn_value in zip( + range(len(turns)), col_values[0], strict=False + ): + turns[turn_idx][col_type] = [turn_value] + else: + for turn_idx in range(len(turns)): + turns[turn_idx][col_type] = [col_values[0]] + return [self._apply(turn_columns) for turn_columns in turns] + else: + return self._apply(columns) + + def _apply( # noqa: C901, PLR0912, PLR0915 + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest: + arguments = GenerationRequestArguments(body={}) + input_metrics = UsageMetrics() + output_metrics = UsageMetrics() + + # Add model + if self.model is not None: + arguments.body["model"] = self.model + + # Configure streaming + if self.stream: + arguments.stream = True + arguments.body.update( + {"stream": True, "stream_options": {"include_usage": True}} + ) + + # Handle output tokens + if output_tokens := sum( + count for count in columns.get("output_tokens_count_column", []) if count + ): + output_metrics.text_tokens = output_tokens + arguments.body.update( + { + "max_completion_tokens": output_tokens, + "stop": None, + "ignore_eos": True, + } + ) + elif self.max_completion_tokens is not None: + arguments.body["max_completion_tokens"] = self.max_completion_tokens + + # Handle prompt tokens + if prompt_tokens := sum( + count for count in columns.get("prompt_tokens_count_column", []) if count + ): + input_metrics.text_tokens = prompt_tokens + + # Apply extra arguments + if self.extras: + arguments.model_combine(self.extras) + + # Build messages + arguments.body["messages"] = [] + + for prefix in columns.get("prefix_column", []): + if not prefix: + continue + + stats = text_stats(prefix) + if (num_chars := stats.get("num_chars")) is not None: + input_metrics.text_characters = ( + input_metrics.text_characters or 0 + ) + num_chars + if (num_words := stats.get("num_words")) is not None: + input_metrics.text_words = (input_metrics.text_words or 0) + num_words + + arguments.body["messages"].append({"role": "system", "content": prefix}) + + for text in columns.get("text_column", []): + if not text: + continue + + stats = text_stats(text) + if (num_chars := stats.get("num_chars")) is not None: + input_metrics.text_characters = ( + input_metrics.text_characters or 0 + ) + num_chars + if (num_words := stats.get("num_words")) is not None: + input_metrics.text_words = (input_metrics.text_words or 0) + num_words + + arguments.body["messages"].append( + {"role": "user", "content": [{"type": "text", "text": text}]} + ) + + for image in columns.get("image_column", []): + if not image: + continue + + image_dict = self.encode_image(image, **self.encode_image_kwargs) + if (image_pixels := image_dict.get("image_pixels")) is not None: + input_metrics.image_pixels = ( + input_metrics.image_pixels or 0 + ) + image_pixels + if (image_bytes := image_dict.get("image_bytes")) is not None: + input_metrics.image_bytes = ( + input_metrics.image_bytes or 0 + ) + image_bytes + + arguments.body["messages"].append( + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": image_dict.get("image")} + ], + } + ) + + for video in columns.get("video_column", []): + if not video: + continue + + video_dict = self.encode_video(video, **self.encode_video_kwargs) + if (video_frames := video_dict.get("video_frames")) is not None: + input_metrics.video_frames = ( + input_metrics.video_frames or 0 + ) + video_frames + if (video_seconds := video_dict.get("video_seconds")) is not None: + input_metrics.video_seconds = ( + input_metrics.video_seconds or 0.0 + ) + video_seconds + if (video_bytes := video_dict.get("video_bytes")) is not None: + input_metrics.video_bytes = ( + input_metrics.video_bytes or 0 + ) + video_bytes + + arguments.body["messages"].append( + { + "role": "user", + "content": [ + {"type": "video_url", "video_url": video_dict.get("video")} + ], + } + ) + + for audio in columns.get("audio_column", []): + if not audio: + continue + + audio_dict = self.encode_audio( + audio, b64encode=True, **self.encode_audio_kwargs + ) + if (audio_samples := audio_dict.get("audio_samples")) is not None: + input_metrics.audio_samples = ( + input_metrics.audio_samples or 0 + ) + audio_samples + if (audio_seconds := audio_dict.get("audio_seconds")) is not None: + input_metrics.audio_seconds = ( + input_metrics.audio_seconds or 0.0 + ) + audio_seconds + if (audio_bytes := audio_dict.get("audio_bytes")) is not None: + input_metrics.audio_bytes = ( + input_metrics.audio_bytes or 0 + ) + audio_bytes + + arguments.body["messages"].append( + { + "role": "user", + "content": [ + { + "type": "input_audio", + "input_audio": { + "data": audio_dict.get("audio"), + "format": audio_dict.get("format"), + }, + } + ], + } + ) + + return GenerationRequest( + request_type="chat_completions", + arguments=arguments, + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + +@PreprocessorRegistry.register("audio_transcriptions") +class GenerativeAudioTranscriptionRequestFormatter(RequestFormatter): + def __init__( + self, + model: str, + extras: dict[str, Any] | GenerationRequestArguments | None = None, + stream: bool = True, + encode_kwargs: dict[str, Any] | None = None, + ): + self.model = model + self.extras = ( + GenerationRequestArguments(**extras) + if extras and isinstance(extras, dict) + else extras + ) + self.stream = stream + self.encode_audio_kwargs = encode_kwargs or {} + + def __call__( # noqa: C901 + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest: + arguments = GenerationRequestArguments(body={}, files={}) + input_metrics = UsageMetrics() + output_metrics = UsageMetrics() + + # Add model + if self.model is not None: + arguments.body["model"] = self.model + + # Configure streaming + if self.stream: + arguments.stream = True + arguments.body["stream"] = True + + # Handle output tokens + if output_tokens := sum( + count for count in columns.get("output_tokens_count_column", []) if count + ): + output_metrics.text_tokens = output_tokens + + # Handle prompt tokens (for audio duration tracking) + if prompt_tokens := sum( + count for count in columns.get("prompt_tokens_count_column", []) if count + ): + input_metrics.text_tokens = prompt_tokens + + # Apply extra arguments + if self.extras: + arguments.model_combine(self.extras) + + # Build audio input + audio_columns = columns.get("audio_column", []) + if len(audio_columns) != 1: + raise ValueError( + f"GenerativeAudioTranscriptionRequestFormatter expects exactly " + f"one audio column, but got {len(audio_columns)}." + ) + + audio_dict = self.encode_audio( + audio_columns[0], b64encode=False, **self.encode_audio_kwargs + ) + input_metrics.audio_samples = audio_dict.get("audio_samples") + input_metrics.audio_seconds = audio_dict.get("audio_seconds") + input_metrics.audio_bytes = audio_dict.get("audio_bytes") + + arguments.files = { + "file": ( + audio_dict.get("file_name", "audio_input"), + audio_dict.get("audio"), + audio_dict.get("mimetype"), + ) + } + + # Build prompt + prefix = "".join(pre for pre in columns.get("prefix_column", []) if pre) + text = "".join(txt for txt in columns.get("text_column", []) if txt) + if prefix or text: + arguments.body["prompt"] = prefix + text + stats = text_stats(arguments.body["prompt"]) + input_metrics.text_characters = stats.get("num_chars") + input_metrics.text_words = stats.get("num_words") + + return GenerationRequest( + request_type="audio_transcriptions", + arguments=arguments, + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + +@PreprocessorRegistry.register("audio_translations") +class GenerativeAudioTranslationRequestFormatter( + GenerativeAudioTranscriptionRequestFormatter +): + def __call__( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest: + result = super().__call__(columns) + result.request_type = "audio_translations" + return result diff --git a/src/guidellm/data/preprocessors/mappers.py b/src/guidellm/data/preprocessors/mappers.py new file mode 100644 index 00000000..0783103b --- /dev/null +++ b/src/guidellm/data/preprocessors/mappers.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from collections import defaultdict +from typing import Any, ClassVar, cast + +from datasets import Dataset, IterableDataset + +from guidellm.data.preprocessors.preprocessor import ( + DataDependentPreprocessor, + PreprocessorRegistry, +) +from guidellm.data.schemas import GenerativeDatasetColumnType + +__all__ = ["GenerativeColumnMapper"] + + +@PreprocessorRegistry.register("generative_column_mapper") +class GenerativeColumnMapper(DataDependentPreprocessor): + defaults: ClassVar[dict[str, list[str]]] = { + "prompt_tokens_count_column": ["prompt_tokens_count", "input_tokens_count"], + "output_tokens_count_column": [ + "output_tokens_count", + "completion_tokens_count", + ], + "prefix_column": [ + "system_prompt", + "system", + "prefix", + ], + "text_column": [ + "prompt", + "instruction", + "question", + "input", + "context", + "content", + "conversation", + "turn", + "text", + ], + "image_column": [ + "image", + "picture", + "photo", + "img", + ], + "video_column": [ + "video", + "clip", + "movie", + "footage", + "mp4", + "mov", + "avi", + ], + "audio_column": [ + "audio", + "sound", + "voice", + "speech", + "wav", + "mp3", + ], + } + + @classmethod + def datasets_default_mappings( + cls, datasets: list[Dataset | IterableDataset] + ) -> dict[GenerativeDatasetColumnType, list[tuple[int, str]]]: + mappings: dict[GenerativeDatasetColumnType, list[tuple[int, str]]] = ( + defaultdict(list) + ) + + for index, dataset in enumerate(datasets): + dataset_columns = dataset.column_names or list(next(iter(dataset)).keys()) + + for column_type in cls.defaults: + if column_type in mappings: + continue + + type_names = [ + variant + for name in cls.defaults.get(column_type, []) + for plural in [name, f"{name}s", f"{name}es"] + for variant in [ + plural, + plural.lower(), + plural.upper(), + plural.capitalize(), + ] + ] + + for name in type_names: + if name in dataset_columns: + key = cast("GenerativeDatasetColumnType", column_type) + mappings[key].append((index, name)) + break + + return mappings + + @classmethod + def datasets_mappings( + cls, + datasets: list[Dataset | IterableDataset], + input_mappings: dict[GenerativeDatasetColumnType, str | list[str]], + ) -> dict[GenerativeDatasetColumnType, list[tuple[int, str]]]: + mappings: dict[GenerativeDatasetColumnType, list[tuple[int, str]]] = ( + defaultdict(list) + ) + datasets_named_indices = { + ( + dataset.info.dataset_name + if dataset.info and dataset.info.dataset_name + else index + ): index + for index, dataset in enumerate(datasets) + } + datasets_columns = { + index: dataset.column_names or list(next(iter(dataset)).keys()) + for index, dataset in enumerate(datasets) + } + + # Parse out user mappings that were passed in and validate them + # Must be in the format of: + # {: []} + # where can be a single string or list of strings + # and each string can be any of: + # - a column name (assumes the first dataset was intended) + # - . where is the dataset index + # - . where is the dataset name + for column_type, names in input_mappings.items(): + mappings[column_type] = [] + for name in names if isinstance(names, list) else [names]: + if "." in name: + dataset, column_name = name.split(".", 1) + dataset_index = ( + int(dataset) + if dataset.isdigit() + else datasets_named_indices.get(dataset) + ) + else: + dataset_index = 0 + column_name = name + + if dataset_index is None or dataset_index >= len(datasets): + raise ValueError( + f"Dataset '{name}' not found in datasets: " + f"{datasets_named_indices}." + ) + if column_name not in datasets_columns[dataset_index]: + raise ValueError( + f"Column '{column_name}' not found in dataset " + f"'{datasets[dataset_index]}' " + f"columns: {datasets_columns[dataset_index]}." + ) + mappings[column_type].append((dataset_index, column_name)) + + return mappings + + def __init__( + self, + column_mappings: dict[GenerativeDatasetColumnType, str | list[str]] + | None = None, + ): + self.input_mappings = column_mappings + self.datasets_column_mappings: ( + dict[GenerativeDatasetColumnType, list[tuple[int, str]]] | None + ) + + def __call__( + self, row: dict[str, Any] + ) -> dict[GenerativeDatasetColumnType, list[Any]]: + if self.datasets_column_mappings is None: + raise ValueError("DefaultGenerativeColumnMapper not setup with data.") + + items = cast("dict[int, dict[str, Any]]", row.pop("items")) + mapped: dict[GenerativeDatasetColumnType, list[Any]] = defaultdict(list) + + for column_type, column_mappings in self.datasets_column_mappings.items(): + for ( + dataset_index, + dataset_column, + ) in column_mappings: + mapped[column_type].append(items[dataset_index][dataset_column]) + + return dict(mapped) + + def setup_data( + self, + datasets: list[Dataset | IterableDataset], + data_args: list[dict[str, Any]], + ): + _ = data_args # Unused for this mapper + self.datasets_column_mappings = ( + self.datasets_default_mappings(datasets) + if self.input_mappings is None + else self.datasets_mappings(datasets, self.input_mappings) + ) diff --git a/src/guidellm/data/preprocessors/preprocessor.py b/src/guidellm/data/preprocessors/preprocessor.py new file mode 100644 index 00000000..eefb53d3 --- /dev/null +++ b/src/guidellm/data/preprocessors/preprocessor.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import Any, Protocol, Union, runtime_checkable + +from datasets import Dataset, IterableDataset + +from guidellm.utils import RegistryMixin + +__all__ = ["DataDependentPreprocessor", "DatasetPreprocessor", "PreprocessorRegistry"] + + +@runtime_checkable +class DatasetPreprocessor(Protocol): + def __call__(self, item: dict[str, Any]) -> dict[str, Any]: ... + + +@runtime_checkable +class DataDependentPreprocessor(DatasetPreprocessor, Protocol): + def setup_data( + self, + datasets: list[Dataset | IterableDataset], + data_args: list[dict[str, Any]], + ): ... + + +class PreprocessorRegistry( + RegistryMixin[Union[DataDependentPreprocessor, type[DataDependentPreprocessor]]] +): + pass diff --git a/src/guidellm/data/prideandprejudice.txt.gz b/src/guidellm/data/prideandprejudice.txt.gz deleted file mode 100644 index 8c7a1072..00000000 Binary files a/src/guidellm/data/prideandprejudice.txt.gz and /dev/null differ diff --git a/src/guidellm/data/processor.py b/src/guidellm/data/processor.py new file mode 100644 index 00000000..645683c4 --- /dev/null +++ b/src/guidellm/data/processor.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Any + +from transformers import ( # type: ignore[import] + AutoTokenizer, + PreTrainedTokenizerBase, +) + +__all__ = ["ProcessorFactory"] + + +class ProcessorFactory: + def __init__( + self, + processor: str | PreTrainedTokenizerBase, + processor_args: dict[str, Any] | None = None, + ) -> None: + self.processor = processor + self.processor_args = processor_args or {} + + def __call__(self) -> PreTrainedTokenizerBase: + if isinstance(self.processor, PreTrainedTokenizerBase): + return self.processor + else: + self.processor = AutoTokenizer.from_pretrained( + self.processor, + **(self.processor_args or {}), + ) + return self.processor diff --git a/src/guidellm/data/schemas.py b/src/guidellm/data/schemas.py new file mode 100644 index 00000000..c4421e07 --- /dev/null +++ b/src/guidellm/data/schemas.py @@ -0,0 +1,13 @@ +from typing import Literal + +__all__ = ["GenerativeDatasetColumnType"] + +GenerativeDatasetColumnType = Literal[ + "prompt_tokens_count_column", + "output_tokens_count_column", + "prefix_column", + "text_column", + "image_column", + "video_column", + "audio_column", +] diff --git a/src/guidellm/data/utils/__init__.py b/src/guidellm/data/utils/__init__.py new file mode 100644 index 00000000..d71e6236 --- /dev/null +++ b/src/guidellm/data/utils/__init__.py @@ -0,0 +1,10 @@ +from .dataset import DEFAULT_SPLITS, resolve_dataset_split +from .functions import ( + text_stats, +) + +__all__ = [ + "DEFAULT_SPLITS", + "resolve_dataset_split", + "text_stats", +] diff --git a/src/guidellm/data/utils/dataset.py b/src/guidellm/data/utils/dataset.py new file mode 100644 index 00000000..9656c1a7 --- /dev/null +++ b/src/guidellm/data/utils/dataset.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from typing import Literal + +from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict + +__all__ = ["DEFAULT_SPLITS", "resolve_dataset_split"] + + +DEFAULT_SPLITS: dict[Literal["train", "calib", "val", "test"], list[str]] = { + "train": [ + "train", + "training", + "train_set", + "training_set", + "train_dataset", + "training_dataset", + "train_data", + "training_data", + "pretrain", + "pretrain_set", + "pretrain_dataset", + "pretrain_data", + "pretraining", + ], + "calib": [ + "calibration", + "calib", + "cal", + "calibration_set", + "calib_set", + "cal_set", + "calibration_dataset", + "calib_dataset", + "cal_set", + "calibration_data", + "calib_data", + "cal_data", + ], + "val": [ + "validation", + "val", + "valid", + "validation_set", + "val_set", + "validation_dataset", + "val_dataset", + "validation_data", + "val_data", + "dev", + "dev_set", + "dev_dataset", + "dev_data", + ], + "test": [ + "test", + "testing", + "test_set", + "testing_set", + "test_dataset", + "testing_dataset", + "test_data", + "testing_data", + "eval", + "eval_set", + "eval_dataset", + "eval_data", + ], +} + + +def resolve_dataset_split( + dataset: Dataset | IterableDataset | DatasetDict | IterableDatasetDict, + split: str | None = None, +) -> Dataset | IterableDataset: + if split is not None and isinstance(dataset, (DatasetDict, IterableDatasetDict)): + if split in dataset: + return dataset[split] + + raise ValueError(f"Requested split '{split}' not found in dataset: {dataset}.") + elif split is not None: + raise ValueError( + f"Requested split '{split}' but dataset has no splits: {dataset}." + ) + + if isinstance(dataset, (Dataset, IterableDataset)): + return dataset + + for _, default_splits in DEFAULT_SPLITS.items(): + for default_split in default_splits: + if default_split in dataset: + return dataset[default_split] + + return dataset[list(dataset.keys())[0]] diff --git a/src/guidellm/data/utils/functions.py b/src/guidellm/data/utils/functions.py new file mode 100644 index 00000000..4260b1f1 --- /dev/null +++ b/src/guidellm/data/utils/functions.py @@ -0,0 +1,18 @@ +from typing import Literal + +__all__ = ["text_stats"] + + +def text_stats( + text: str, +) -> dict[Literal["type", "text", "num_chars", "num_words"], str | int]: + """Compute basic text statistics.""" + num_chars = len(text) + num_words = len(text.split()) + + return { + "type": "text", + "text": text, + "num_chars": num_chars, + "num_words": num_words, + } diff --git a/src/guidellm/dataset/__init__.py b/src/guidellm/dataset/__init__.py deleted file mode 100644 index b90b72ff..00000000 --- a/src/guidellm/dataset/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from .creator import ColumnInputTypes, DatasetCreator -from .entrypoints import load_dataset -from .file import FileDatasetCreator -from .hf_datasets import HFDatasetsCreator -from .in_memory import InMemoryDatasetCreator -from .synthetic import ( - SyntheticDatasetConfig, - SyntheticDatasetCreator, - SyntheticTextItemsGenerator, -) - -__all__ = [ - "ColumnInputTypes", - "DatasetCreator", - "FileDatasetCreator", - "HFDatasetsCreator", - "InMemoryDatasetCreator", - "SyntheticDatasetConfig", - "SyntheticDatasetCreator", - "SyntheticTextItemsGenerator", - "load_dataset", -] diff --git a/src/guidellm/dataset/creator.py b/src/guidellm/dataset/creator.py deleted file mode 100644 index a74ec8c0..00000000 --- a/src/guidellm/dataset/creator.py +++ /dev/null @@ -1,213 +0,0 @@ -from abc import ABC, abstractmethod -from pathlib import Path -from typing import Any, Literal, Optional, Union - -from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -__all__ = ["ColumnInputTypes", "DatasetCreator"] - -ColumnInputTypes = Literal[ - "prompt_column", - "text_column", - "prompt_tokens_count_column", - "output_tokens_count_column", -] - - -class DatasetCreator(ABC): - DEFAULT_SPLITS_TRAIN = [ - "train", - "training", - "train_set", - "training_set", - "train_dataset", - "training_dataset", - "train_data", - "training_data", - "pretrain", - "pretrain_set", - "pretrain_dataset", - "pretrain_data", - "pretraining", - ] - DEFAULT_SPLITS_CALIB = [ - "calibration", - "calib", - "cal", - "calibration_set", - "calib_set", - "cal_set", - "calibration_dataset", - "calib_dataset", - "cal_set", - "calibration_data", - "calib_data", - "cal_data", - ] - DEFAULT_SPLITS_VAL = [ - "validation", - "val", - "valid", - "validation_set", - "val_set", - "validation_dataset", - "val_dataset", - "validation_data", - "val_data", - "dev", - "dev_set", - "dev_dataset", - "dev_data", - ] - DEFAULT_SPLITS_TEST = [ - "test", - "testing", - "test_set", - "testing_set", - "test_dataset", - "testing_dataset", - "test_data", - "testing_data", - "eval", - "eval_set", - "eval_dataset", - "eval_data", - ] - DEFAULT_SPLITS_DATASET: dict[str, str] = {} - - @classmethod - def create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - random_seed: int = 42, - split_pref_order: Optional[list[str]] = None, - ) -> tuple[Union[Dataset, IterableDataset], dict[ColumnInputTypes, str]]: - if not cls.is_supported(data, data_args): - raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") - - split = cls.extract_args_split(data_args) - column_mappings = cls.extract_args_column_mappings(data_args) - dataset = cls.handle_create( - data, data_args, processor, processor_args, random_seed - ) - - if isinstance(dataset, (DatasetDict, IterableDatasetDict)): - dataset = cls.extract_dataset_split(dataset, split, split_pref_order) - - if not isinstance(dataset, (Dataset, IterableDataset)): - raise ValueError( - f"Unsupported data type: {type(dataset)} given for {dataset}." - ) - - return dataset, column_mappings - - @classmethod - def extract_args_split(cls, data_args: Optional[dict[str, Any]]) -> str: - split = "auto" - - if data_args and "split" in data_args: - split = data_args["split"] - del data_args["split"] - - return split - - @classmethod - def extract_args_column_mappings( - cls, - data_args: Optional[dict[str, Any]], - ) -> dict[ColumnInputTypes, str]: - columns: dict[ColumnInputTypes, str] = {} - - if data_args: - if "prompt_column" in data_args: - columns["prompt_column"] = data_args["prompt_column"] - del data_args["prompt_column"] - - if "prompt_tokens_count_column" in data_args: - columns["prompt_tokens_count_column"] = data_args[ - "prompt_tokens_count_column" - ] - del data_args["prompt_tokens_count_column"] - - if "output_tokens_count_column" in data_args: - columns["output_tokens_count_column"] = data_args[ - "output_tokens_count_column" - ] - del data_args["output_tokens_count_column"] - - return columns - - @classmethod - def extract_dataset_name( - cls, dataset: Union[Dataset, IterableDataset, DatasetDict, IterableDatasetDict] - ) -> Optional[str]: - if isinstance(dataset, (DatasetDict, IterableDatasetDict)): - dataset = dataset[list(dataset.keys())[0]] - - if isinstance(dataset, (Dataset, IterableDataset)): - if not hasattr(dataset, "info") or not hasattr( - dataset.info, "dataset_name" - ): - return None - - return dataset.info.dataset_name - - raise ValueError(f"Unsupported data type: {type(dataset)} given for {dataset}.") - - @classmethod - def extract_dataset_split( - cls, - dataset: Union[DatasetDict, IterableDatasetDict], - specified_split: Union[Literal["auto"], str] = "auto", - split_pref_order: Optional[Union[Literal["auto"], list[str]]] = "auto", - ) -> Union[Dataset, IterableDataset]: - if not isinstance(dataset, (DatasetDict, IterableDatasetDict)): - raise ValueError( - f"Unsupported data type: {type(dataset)} given for {dataset}." - ) - - if specified_split != "auto": - if specified_split not in dataset: - raise ValueError( - f"Split {specified_split} not found in dataset {dataset}." - ) - - return dataset[specified_split] - - dataset_name = cls.extract_dataset_name(dataset) - - if dataset_name and dataset_name in cls.DEFAULT_SPLITS_DATASET: - return dataset[cls.DEFAULT_SPLITS_DATASET[dataset_name]] - - if split_pref_order == "auto": - split_pref_order = [ - *cls.DEFAULT_SPLITS_TEST, - *cls.DEFAULT_SPLITS_VAL, - *cls.DEFAULT_SPLITS_CALIB, - *cls.DEFAULT_SPLITS_TRAIN, - ] - - for test_split in split_pref_order or []: - if test_split in dataset: - return dataset[test_split] - - return dataset[list(dataset.keys())[0]] - - @classmethod - @abstractmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: ... - - @classmethod - @abstractmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - random_seed: int, - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: ... diff --git a/src/guidellm/dataset/entrypoints.py b/src/guidellm/dataset/entrypoints.py deleted file mode 100644 index cf689956..00000000 --- a/src/guidellm/dataset/entrypoints.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path -from typing import Any, Optional, Union - -from datasets import Dataset, IterableDataset -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import ColumnInputTypes -from guidellm.dataset.file import FileDatasetCreator -from guidellm.dataset.hf_datasets import HFDatasetsCreator -from guidellm.dataset.in_memory import InMemoryDatasetCreator -from guidellm.dataset.synthetic import SyntheticDatasetCreator - -__all__ = ["load_dataset"] - - -def load_dataset( - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - random_seed: int = 42, - split_pref_order: Optional[list[str]] = None, -) -> tuple[Union[Dataset, IterableDataset], dict[ColumnInputTypes, str]]: - creators = [ - InMemoryDatasetCreator, - SyntheticDatasetCreator, - FileDatasetCreator, - HFDatasetsCreator, - ] - - for creator in creators: - if creator.is_supported(data, data_args): - return creator.create( - data, - data_args, - processor, - processor_args, - random_seed, - split_pref_order, - ) - - raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") diff --git a/src/guidellm/dataset/file.py b/src/guidellm/dataset/file.py deleted file mode 100644 index 5d6df1d9..00000000 --- a/src/guidellm/dataset/file.py +++ /dev/null @@ -1,92 +0,0 @@ -from pathlib import Path -from typing import Any, Optional, Union - -import pandas as pd # type: ignore[import] -from datasets import ( - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, - load_dataset, -) -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import DatasetCreator - -__all__ = ["FileDatasetCreator"] - - -class FileDatasetCreator(DatasetCreator): - SUPPORTED_TYPES = { - ".txt", - ".text", - ".csv", - ".json", - ".jsonl", - ".parquet", - ".arrow", - ".hdf5", - ".tar", - } - - @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 - if isinstance(data, (str, Path)) and (path := Path(data)).exists(): - # local folder or py file, assume supported - return path.suffix.lower() in cls.SUPPORTED_TYPES - - return False - - @classmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 - random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - if not isinstance(data, (str, Path)): - raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") - - path = Path(data) - if not path.exists(): - raise FileNotFoundError(f"File not found: {path}") - - if not path.is_file(): - raise ValueError(f"Unsupported data type: {path} given for {path}. ") - - if path.suffix.lower() not in cls.SUPPORTED_TYPES: - raise ValueError(f"Unsupported file type: {path.suffix} given for {path}. ") - - return cls.load_dataset(path, data_args) - - @classmethod - def load_dataset( - cls, path: Path, data_args: Optional[dict[str, Any]] - ) -> Union[Dataset, IterableDataset]: - if path.suffix.lower() in {".txt", ".text"}: - with path.open("r") as file: - items = file.readlines() - - dataset = Dataset.from_dict({"text": items}, **(data_args or {})) - elif path.suffix.lower() == ".csv": - dataset = load_dataset("csv", data_files=str(path), **(data_args or {})) - elif path.suffix.lower() in {".json", ".jsonl"}: - dataset = load_dataset("json", data_files=str(path), **(data_args or {})) - elif path.suffix.lower() == ".parquet": - dataset = load_dataset("parquet", data_files=str(path), **(data_args or {})) - elif path.suffix.lower() == ".arrow": - dataset = load_dataset("arrow", data_files=str(path), **(data_args or {})) - elif path.suffix.lower() == ".hdf5": - dataset = Dataset.from_pandas(pd.read_hdf(str(path)), **(data_args or {})) - elif path.suffix.lower() == ".db": - dataset = Dataset.from_sql(con=str(path), **(data_args or {})) - elif path.suffix.lower() == ".tar": - dataset = load_dataset( - "webdataset", data_files=str(path), **(data_args or {}) - ) - else: - raise ValueError(f"Unsupported file type: {path.suffix} given for {path}. ") - - return dataset diff --git a/src/guidellm/dataset/hf_datasets.py b/src/guidellm/dataset/hf_datasets.py deleted file mode 100644 index 7f91facd..00000000 --- a/src/guidellm/dataset/hf_datasets.py +++ /dev/null @@ -1,62 +0,0 @@ -from pathlib import Path -from typing import Any, Optional, Union - -from datasets import ( - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, - get_dataset_config_info, - load_dataset, -) -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import DatasetCreator - -__all__ = ["HFDatasetsCreator"] - - -class HFDatasetsCreator(DatasetCreator): - @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 - if isinstance( - data, (Dataset, DatasetDict, IterableDataset, IterableDatasetDict) - ): - # base type is supported - return True - - if isinstance(data, (str, Path)) and (path := Path(data)).exists(): - # local folder or py file, assume supported - return path.is_dir() or path.suffix == ".py" - - if isinstance(data, (str, Path)): - try: - # try to load dataset - return get_dataset_config_info(data) is not None - except Exception: # noqa: BLE001, S110 - pass - - return False - - @classmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 - random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - if isinstance(data, (str, Path)): - data = load_dataset(data, **(data_args or {})) - elif data_args: - raise ValueError( - f"data_args should not be provided when data is a {type(data)}" - ) - - if isinstance( - data, (Dataset, DatasetDict, IterableDataset, IterableDatasetDict) - ): - return data - - raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") diff --git a/src/guidellm/dataset/in_memory.py b/src/guidellm/dataset/in_memory.py deleted file mode 100644 index af84f658..00000000 --- a/src/guidellm/dataset/in_memory.py +++ /dev/null @@ -1,132 +0,0 @@ -from collections.abc import Iterable -from pathlib import Path -from typing import Any, Optional, Union - -from datasets import ( - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, -) -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import DatasetCreator - -__all__ = ["InMemoryDatasetCreator"] - - -class InMemoryDatasetCreator(DatasetCreator): - @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 - return isinstance(data, Iterable) and not isinstance(data, str) - - @classmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 - random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - if not isinstance(data, Iterable): - raise TypeError( - f"Unsupported data format. Expected Iterable[Any], got {type(data)}" - ) - - if not data: - raise ValueError("Data is empty") - - if isinstance(data, dict): - # assume data is a dictionary of columns and values: {"c1": ["i1", "i2"]} - data_dict = cls.format_data_dict(data) - elif isinstance(data[0], dict): # type: ignore[index] - # assume data is a list of dictionaries: [{"c1": "i1"}, {"c1": "i2"}] - data_dict = cls.format_data_iterable_dicts(data) - else: - # assume data is a list of items with no columns: ["i1", "i2"] - data_dict = cls.format_data_iterable_values(data) - - return Dataset.from_dict(data_dict, **(data_args or {})) - - @classmethod - def format_data_dict(cls, data: dict[Any, Any]) -> dict[str, Any]: - if not isinstance(data, dict): - raise TypeError( - f"Unsupported data format. Expected Dict[str, Iterable[Any]], " - f"got {type(data)}" - ) - - if not all( - isinstance(key, str) and isinstance(val, Iterable) - for key, val in data.items() - ): - raise TypeError( - "Unsupported data format. Expected Dict[str, Iterable[Any]], " - f"got {type(data)}" - ) - - samples = len(list(data.values())[0]) - if not all(len(val) == samples for val in data.values()): - raise ValueError( - "Unsupported data format. Not all columns have the same number samples " - f"for {data}" - ) - - return data - - @classmethod - def format_data_iterable_dicts( - cls, data: Iterable[dict[Any, Any]] - ) -> dict[str, Any]: - if not isinstance(data, Iterable): - raise TypeError( - f"Unsupported data format. Expected Iterable[Dict[str, Any]], " - f"got {type(data)}" - ) - - if not all(isinstance(item, dict) for item in data): - raise TypeError( - f"Unsupported data format. Expected Iterable[Dict[str, Any]], " - f"got {type(data)}" - ) - - if not all(isinstance(key, str) for key in data[0]): # type: ignore[index] - raise TypeError( - "Unsupported data format. Expected Dict[str, Any], " - f"but one of the items had a non string column for {data}" - ) - - columns = list(data[0].keys()) # type: ignore[index] - if not all( - len(item) == len(columns) and all(key in item for key in columns) - for item in data - ): - raise ValueError( - "Unsupported data format. Not all items have the same columns " - f"for {data}" - ) - - data_dict: dict[str, Any] = {key: [] for key in columns} - for item in data: - for key, value in item.items(): - data_dict[key].append(value) - - return data_dict - - @classmethod - def format_data_iterable_values(cls, data: Iterable[Any]) -> dict[str, Any]: - if not isinstance(data, Iterable): - raise TypeError( - f"Unsupported data format. Expected Iterable[Iterable[Any]], " - f"got {type(data)}" - ) - - first_item = next(iter(data), None) - first_type = type(first_item) - if not all(isinstance(item, first_type) for item in data): - raise TypeError( - f"Unsupported data format. Not all types are the same for {data}" - ) - - return {"data": list(data)} diff --git a/src/guidellm/dataset/synthetic.py b/src/guidellm/dataset/synthetic.py deleted file mode 100644 index 8c30f0f7..00000000 --- a/src/guidellm/dataset/synthetic.py +++ /dev/null @@ -1,287 +0,0 @@ -import json -import random -from collections.abc import Iterable, Iterator -from itertools import cycle -from pathlib import Path -from typing import Any, Literal, Optional, Union - -import yaml -from datasets import ( - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, -) -from pydantic import BaseModel, Field -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import ColumnInputTypes, DatasetCreator -from guidellm.utils import EndlessTextCreator, IntegerRangeSampler, check_load_processor - -__all__ = [ - "SyntheticDatasetConfig", - "SyntheticDatasetCreator", - "SyntheticTextItemsGenerator", -] - - -class SyntheticDatasetConfig(BaseModel): - prefix_tokens: int = Field( - description="The number of shared prefix tokens to prepend to each prompt.", - ge=0, - default=0, - ) - prompt_tokens: int = Field( - description="The average number of text tokens generated for prompts.", - gt=0, - ) - prompt_tokens_stdev: Optional[int] = Field( - description="The standard deviation of the tokens generated for prompts.", - gt=0, - default=None, - ) - prompt_tokens_min: Optional[int] = Field( - description="The minimum number of text tokens generated for prompts.", - gt=0, - default=None, - ) - prompt_tokens_max: Optional[int] = Field( - description="The maximum number of text tokens generated for prompts.", - gt=0, - default=None, - ) - output_tokens: int = Field( - description="The average number of text tokens generated for outputs.", - gt=0, - ) - output_tokens_stdev: Optional[int] = Field( - description="The standard deviation of the tokens generated for outputs.", - gt=0, - default=None, - ) - output_tokens_min: Optional[int] = Field( - description="The minimum number of text tokens generated for outputs.", - gt=0, - default=None, - ) - output_tokens_max: Optional[int] = Field( - description="The maximum number of text tokens generated for outputs.", - gt=0, - default=None, - ) - samples: int = Field( - description="The number of samples to generate for the dataset.", - gt=0, - default=1000, - ) - source: str = Field( - description="The source of the text data to be used for generation.", - default="data:prideandprejudice.txt.gz", - ) - - @staticmethod - def parse_str(data: Union[str, Path]) -> "SyntheticDatasetConfig": - if ( - isinstance(data, Path) - or data.strip().endswith(".config") - or data.strip().endswith(".yaml") - ): - return SyntheticDatasetConfig.parse_config_file(data) - - if data.strip().startswith("{"): - return SyntheticDatasetConfig.parse_json(data) - - if data.count("=") > 1: - return SyntheticDatasetConfig.parse_key_value_pairs(data) - - raise ValueError( - f"Unsupported data format. Expected JSON or key-value pairs, got {data}" - ) - - @staticmethod - def parse_json(data: str) -> "SyntheticDatasetConfig": - config_dict = json.loads(data.strip()) - - return SyntheticDatasetConfig(**config_dict) - - @staticmethod - def parse_key_value_pairs(data: str) -> "SyntheticDatasetConfig": - config_dict = {} - items = data.strip().split(",") - for item in items: - key, value = item.split("=") - config_dict[key.strip()] = ( - int(value.strip()) if value.strip().isnumeric() else value.strip() - ) - - return SyntheticDatasetConfig(**config_dict) # type: ignore[arg-type] - - @staticmethod - def parse_config_file(data: Union[str, Path]) -> "SyntheticDatasetConfig": - with Path(data).open("r") as file: - config_dict = yaml.safe_load(file) - - return SyntheticDatasetConfig(**config_dict) - - -class SyntheticTextItemsGenerator( - Iterable[ - dict[ - Literal["prompt", "prompt_tokens_count", "output_tokens_count"], - Union[str, int], - ] - ] -): - def __init__( - self, - config: SyntheticDatasetConfig, - processor: PreTrainedTokenizerBase, - random_seed: int, - ): - self.config = config - self.processor = processor - self.random_seed = random_seed - self.text_creator = EndlessTextCreator( - data=config.source, - ) - - def __iter__( - self, - ) -> Iterator[ - dict[ - Literal["prompt", "prompt_tokens_count", "output_tokens_count"], - Union[str, int], - ] - ]: - prompt_tokens_sampler = IntegerRangeSampler( - average=self.config.prompt_tokens, - variance=self.config.prompt_tokens_stdev, - min_value=self.config.prompt_tokens_min, - max_value=self.config.prompt_tokens_max, - random_seed=self.random_seed, - ) - output_tokens_sampler = IntegerRangeSampler( - average=self.config.output_tokens, - variance=self.config.output_tokens_stdev, - min_value=self.config.output_tokens_min, - max_value=self.config.output_tokens_max, - random_seed=self.random_seed + 1, # ensure diff dist from prompts - ) - # ensure diff distribution from output tokens - rand = random.Random(self.random_seed + 2) # noqa: S311 - unique_prefix_iter = cycle(self.processor.get_vocab().values()) - - prefix_index = rand.randint(0, len(self.text_creator.words)) - prefix_tokens = self._create_prompt(self.config.prefix_tokens, prefix_index) - - for _, prompt_tokens, output_tokens in zip( - range(self.config.samples), - prompt_tokens_sampler, - output_tokens_sampler, - ): - start_index = rand.randint(0, len(self.text_creator.words)) - prompt_text = self.processor.decode( - prefix_tokens - + self._create_prompt( - prompt_tokens, start_index, next(unique_prefix_iter) - ), - skip_special_tokens=True, - ) - yield { - "prompt": prompt_text, - "prompt_tokens_count": self.config.prefix_tokens + prompt_tokens, - "output_tokens_count": output_tokens, - } - - def _create_prompt( - self, prompt_tokens: int, start_index: int, unique_prefix: Optional[int] = None - ) -> list[int]: - if prompt_tokens <= 0: - return [] - - left = start_index - right = start_index + 4 * prompt_tokens - start_tokens = [unique_prefix] if unique_prefix else [] - - while left < right: - mid = (left + right) // 2 - test_prompt = self.text_creator.create_text(start_index, mid - start_index) - test_tokens = start_tokens + self.processor.encode(test_prompt) - - if len(test_tokens) == prompt_tokens: - return test_tokens - elif len(test_tokens) < prompt_tokens: - left = mid + 1 - else: - right = mid - - final_text = self.text_creator.create_text(start_index, left - start_index) - return start_tokens + self.processor.encode(final_text) - - -class SyntheticDatasetCreator(DatasetCreator): - @classmethod - def is_supported( - cls, - data: Any, - data_args: Optional[dict[str, Any]], # noqa: ARG003 - ) -> bool: - if ( - isinstance(data, Path) - and data.exists() - and data.suffix in {".config", ".yaml"} - ): - return True - - if isinstance(data, str): - data_str: str = data.strip() - if ( - data_str.startswith("{") - or data_str.count("=") > 1 - or data_str.endswith((".config", ".yaml")) - ): - return True - - return False - - @classmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - random_seed: int, - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - processor = check_load_processor( - processor, - processor_args, - error_msg=( - "Processor/tokenizer required for synthetic dataset generation." - ), - ) - - config = SyntheticDatasetConfig.parse_str(data) - generator = SyntheticTextItemsGenerator(config, processor, random_seed) - items = list(generator) - - return Dataset.from_list(items, **(data_args or {})) - - @classmethod - def extract_args_column_mappings( - cls, - data_args: Optional[dict[str, Any]], - ) -> dict[ColumnInputTypes, str]: - data_args_columns = super().extract_args_column_mappings(data_args) - - if data_args_columns: - raise ValueError( - f"Column mappings are not supported for synthetic datasets. " - f"Got {data_args_columns}" - ) - - return { - "prompt_column": "prompt", - "prompt_tokens_count_column": "prompt_tokens_count", - "output_tokens_count_column": "output_tokens_count", - } diff --git a/src/guidellm/extras/__init__.py b/src/guidellm/extras/__init__.py new file mode 100644 index 00000000..80a9a3ea --- /dev/null +++ b/src/guidellm/extras/__init__.py @@ -0,0 +1,4 @@ +""" +Code that depends on optional dependencies. +Each submodule should be deferred imported. +""" diff --git a/src/guidellm/extras/audio.py b/src/guidellm/extras/audio.py new file mode 100644 index 00000000..8d7e7de9 --- /dev/null +++ b/src/guidellm/extras/audio.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import base64 +from pathlib import Path +from typing import Any, Literal + +import httpx +import numpy as np +import torch + +try: + from torchcodec import AudioSamples + from torchcodec.decoders import AudioDecoder + from torchcodec.encoders import AudioEncoder +except ImportError as e: + raise ImportError("Please install guidellm[audio] to use audio features") from e + +__all__ = [ + "encode_audio", + "is_url", +] + + +def is_url(text: Any) -> bool: + return isinstance(text, str) and text.startswith(("http://", "https://")) + + +def encode_audio( + audio: AudioDecoder + | bytes + | str + | Path + | np.ndarray + | torch.Tensor + | dict[str, Any], + b64encode: bool = False, + sample_rate: int | None = None, + file_name: str = "audio.wav", + encode_sample_rate: int = 16000, + max_duration: float | None = None, + mono: bool = True, + audio_format: str = "mp3", + bitrate: str = "64k", +) -> dict[ + Literal[ + "type", + "audio", + "format", + "mimetype", + "audio_samples", + "audio_seconds", + "audio_bytes", + "file_name", + ], + str | int | float | bytes | None, +]: + """Decode audio (if necessary) and re-encode to specified format.""" + samples = _decode_audio(audio, sample_rate=sample_rate, max_duration=max_duration) + + bitrate_val = ( + int(bitrate.rstrip("k")) * 1000 if bitrate.endswith("k") else int(bitrate) + ) + format_val = audio_format.lower() + + encoded_audio = _encode_audio( + samples=samples, + resample_rate=encode_sample_rate, + bitrate=bitrate_val, + audio_format=format_val, + mono=mono, + ) + + return { + "type": "audio_base64" if b64encode else "audio_file", + "audio": ( + base64.b64encode(encoded_audio).decode("utf-8") + if b64encode + else encoded_audio + ), + "file_name": get_file_name(audio) + if isinstance(audio, str | Path) + else file_name, + "format": audio_format, + "mimetype": f"audio/{format_val}", + "audio_samples": samples.sample_rate, + "audio_seconds": samples.duration_seconds, + "audio_bytes": len(encoded_audio), + } + + +def _decode_audio( # noqa: C901, PLR0912 + audio: AudioDecoder + | bytes + | str + | Path + | np.ndarray + | torch.Tensor + | dict[str, Any], + sample_rate: int | None = None, + max_duration: float | None = None, +) -> AudioSamples: + """Decode audio from various input types into AudioSamples.""" + # If input is a dict, unwrap it into a function call + if isinstance(audio, dict): + sample_rate = audio.get("sample_rate", audio.get("sampling_rate", sample_rate)) + if "data" not in audio and "url" not in audio: + raise ValueError( + f"Audio dict must contain either 'data' or 'url' keys, got {audio}" + ) + return _decode_audio( + audio=audio.get("data") or audio.get("url"), + sample_rate=sample_rate, + max_duration=max_duration, + ) + + # Convert numpy array to torch tensor and re-call + if isinstance(audio, np.ndarray): + return _decode_audio( + audio=torch.from_numpy(audio), + sample_rate=sample_rate, + max_duration=max_duration, + ) + + samples: AudioSamples + + data: torch.Tensor | bytes + # HF datasets return AudioDecoder for audio column + if isinstance(audio, AudioDecoder): + samples = audio.get_samples_played_in_range(stop_seconds=max_duration) + elif isinstance(audio, torch.Tensor): + # If float stream assume decoded audio + if torch.is_floating_point(audio): + if sample_rate is None: + raise ValueError("Sample rate must be set for decoded audio") + + full_duration = audio.shape[1] / sample_rate + # If max_duration is set, trim the audio to that duration + if max_duration is not None: + num_samples = int(max_duration * sample_rate) + duration = min(max_duration, full_duration) + data = audio[:, :num_samples] + else: + duration = full_duration + data = audio + + samples = AudioSamples( + data=data, + pts_seconds=0.0, + duration_seconds=duration, + sample_rate=sample_rate, + ) + # If bytes tensor assume encoded audio + elif audio.dtype == torch.uint8: + decoder = AudioDecoder( + source=audio, + sample_rate=sample_rate, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + + else: + raise ValueError(f"Unsupported audio type: {type(audio)}") + + # If bytes, assume encoded audio + elif isinstance(audio, bytes): + decoder = AudioDecoder( + source=audio, + sample_rate=sample_rate, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + + # If str or Path, assume file path or URL to encoded audio + elif isinstance(audio, str | Path): + if isinstance(audio, str) and is_url(audio): + response = httpx.get(audio) + response.raise_for_status() + data = response.content + else: + if not Path(audio).exists(): + raise ValueError(f"Audio file does not exist: {audio}") + data = Path(audio).read_bytes() + decoder = AudioDecoder( + source=data, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + else: + raise ValueError(f"Unsupported audio type: {type(audio)}") + + return samples + + +def _encode_audio( + samples: AudioSamples, + resample_rate: int | None = None, + bitrate: int = 64000, + audio_format: str = "mp3", + mono: bool = True, +) -> bytes: + encoder = AudioEncoder( + samples=samples.data, + sample_rate=samples.sample_rate, + ) + + audio_tensor = encoder.to_tensor( + format=audio_format, + bit_rate=bitrate if audio_format == "mp3" else None, + num_channels=1 if mono else None, + sample_rate=resample_rate, + ) + + return audio_tensor.numpy().tobytes() + + +def get_file_name(path: Path | str) -> str: + """Get file name from path.""" + return Path(path).name diff --git a/src/guidellm/extras/vision.py b/src/guidellm/extras/vision.py new file mode 100644 index 00000000..035c699f --- /dev/null +++ b/src/guidellm/extras/vision.py @@ -0,0 +1,242 @@ +from __future__ import annotations + +import base64 +import io +from pathlib import Path +from typing import Any, Literal + +import httpx +import numpy as np + +try: + from PIL import Image as PILImage +except ImportError as e: + raise ImportError( + "Please install guidellm[vision] to use image/video features" + ) from e + +__all__ = [ + "encode_image", + "encode_video", + "get_file_format", + "is_url", + "resize_image", +] + + +def is_url(text: Any) -> bool: + return isinstance(text, str) and text.startswith(("http://", "https://")) + + +def encode_image( + image: bytes | str | Path | np.ndarray | PILImage.Image, + width: int | None = None, + height: int | None = None, + max_size: int | None = None, + max_width: int | None = None, + max_height: int | None = None, + encode_type: Literal["base64", "url"] | None = "base64", +) -> dict[Literal["type", "image", "image_pixels", "image_bytes"], str | int | None]: + """ + Input image types: + - bytes: raw image bytes, decoded with Pillow + - str: file path on disk, url, or already base64 encoded image string + - pathlib.Path: file path on disk + - np.ndarray: image array, decoded with Pillow + - PIL.Image.Image: Pillow image + - datasets.Image: HuggingFace datasets Image object + + max_size: maximum size of the longest edge of the image + max_width: maximum width of the image + max_height: maximum height of the image + + encode_type: None to return the supported format + (url for url, base64 string for others) + "base64" to return base64 encoded string (or download URL and encode) + "url" to return url (only if input is url, otherwise fails) + + Returns a str of either: + - image url + - "data:image/{type};base64, {data}" string + """ + if isinstance(image, str) and is_url(image): + if encode_type == "base64": + response = httpx.get(image) + response.raise_for_status() + return encode_image( + image=response.content, + max_size=max_size, + max_width=max_width, + max_height=max_height, + encode_type="base64", + ) + + if any([width, height, max_size, max_width, max_height]): + raise ValueError(f"Cannot resize image {image} when encode_type is 'url'") + + return { + "type": "image_url", + "image": image, + "image_pixels": None, + "image_bytes": None, + } + + decoded_image: PILImage.Image + + if isinstance(image, bytes): + decoded_image = PILImage.open(io.BytesIO(image)) + elif isinstance(image, str) and image.startswith("data:image/"): + _, encoded = image.split(",", 1) + image_data = base64.b64decode(encoded) + decoded_image = PILImage.open(io.BytesIO(image_data)) + elif isinstance(image, str | Path): + decoded_image = PILImage.open(image) + elif isinstance(image, np.ndarray): + decoded_image = PILImage.fromarray(image) + elif isinstance(image, PILImage.Image): + decoded_image = image + else: + raise ValueError(f"Unsupported image type: {type(image)} for {image}") + + output_image = resize_image( + decoded_image, + width=width, + height=height, + max_width=max_width, + max_height=max_height, + max_size=max_size, + ) + if output_image.mode != "RGB": + output_image = output_image.convert("RGB") + + buffer = io.BytesIO() + output_image.save(buffer, format="JPEG") + image_bytes = buffer.getvalue() + image_base64 = base64.b64encode(image_bytes).decode("utf-8") + + return { + "type": "image_base64", + "image": f"data:image/jpeg;base64,{image_base64}", + "image_pixels": output_image.width * output_image.height, + "image_bytes": len(image_bytes), + } + + +def resize_image( + image: PILImage.Image, + width: int | None = None, + height: int | None = None, + max_width: int | None = None, + max_height: int | None = None, + max_size: int | None = None, +) -> PILImage.Image: + if not isinstance(image, PILImage.Image): + raise ValueError(f"Unsupported image type: {type(image)}") + + if width is not None and height is not None: + return image.resize((width, height), PILImage.Resampling.BILINEAR) + + orig_w, orig_h = image.size + aspect = orig_w / orig_h + + if width is not None: + target_w = width + target_h = round(width / aspect) + elif height is not None: + target_h = height + target_w = round(height * aspect) + else: + target_w, target_h = orig_w, orig_h + + # Normalize max_size → max_width/max_height + if max_size is not None: + max_width = max_width or max_size + max_height = max_height or max_size + + # Apply max constraints (preserve aspect ratio) + if max_width or max_height: + scale_w = max_width / target_w if max_width else 1.0 + scale_h = max_height / target_h if max_height else 1.0 + scale = min(scale_w, scale_h, 1.0) # never upscale + target_w = round(target_w * scale) + target_h = round(target_h * scale) + + if (target_w, target_h) != (orig_w, orig_h): + image = image.resize((target_w, target_h), PILImage.Resampling.BILINEAR) + + return image + + +def encode_video( + video: bytes | str | Path, + encode_type: Literal["base64", "url"] | None = "base64", +) -> dict[ + Literal["type", "video", "video_frames", "video_seconds", "video_bytes"], + str | int | float | None, +]: + """ + Input video types: + - bytes: raw video bytes + - str: file path on disk, url, or already base64 encoded video string + - pathlib.Path: file path on disk + - datasets.Video: HuggingFace datasets Video object + + encode_type: None to return the supported format + (url for url, base64 string for others) + "base64" to return base64 encoded string (or download URL and encode) + "url" to return url (only if input is url, otherwise fails) + + Returns a str of either: + - video url + - "data:video/{type};base64, {data}" string + """ + if isinstance(video, str) and is_url(video): + if encode_type == "base64": + response = httpx.get(video) + response.raise_for_status() + return encode_video(video=response.content, encode_type="base64") + + return { + "type": "video_url", + "video": video, + "video_frames": None, + "video_seconds": None, + "video_bytes": None, + } + + if isinstance(video, str) and video.startswith("data:video/"): + data_str = video.split(",", 1)[1] + + return { + "type": "video_base64", + "video": video, + "video_frames": None, + "video_seconds": None, + "video_bytes": len(data_str) * 3 // 4, # base64 to bytes + } + + if isinstance(video, str | Path): + path = Path(video) + video_bytes = path.read_bytes() + video_format = get_file_format(path) + elif isinstance(video, bytes): + video_bytes = video + video_format = "unknown" + else: + raise ValueError(f"Unsupported video type: {type(video)} for {video}") + + video_base64 = base64.b64encode(video_bytes).decode("utf-8") + + return { + "type": "video_base64", + "video": f"data:video/{video_format};base64,{video_base64}", + "video_frames": None, + "video_seconds": None, + "video_bytes": len(video_bytes), + } + + +def get_file_format(path: Path | str) -> str: + """Get file format from path extension.""" + suffix = Path(path).suffix.lower() + return suffix[1:] if suffix.startswith(".") else "unknown" diff --git a/src/guidellm/logger.py b/src/guidellm/logger.py index 70259bad..da3464f9 100644 --- a/src/guidellm/logger.py +++ b/src/guidellm/logger.py @@ -72,7 +72,7 @@ def configure_logger(config: LoggingSettings = settings.logging): sys.stdout, level=config.console_log_level.upper(), format="{time:YY-MM-DD HH:mm:ss}|{level: <8} \ - |{name}:{function}:{line} - {message}" + |{name}:{function}:{line} - {message}", ) if config.log_file or config.log_file_level: diff --git a/src/guidellm/mock_server/handlers/chat_completions.py b/src/guidellm/mock_server/handlers/chat_completions.py index de2781b0..5f198a31 100644 --- a/src/guidellm/mock_server/handlers/chat_completions.py +++ b/src/guidellm/mock_server/handlers/chat_completions.py @@ -136,7 +136,7 @@ async def _handle_non_stream(self, req: ChatCompletionsRequest) -> HTTPResponse: # Token counts prompt_text = self.tokenizer.apply_chat_template(req.messages) - prompt_tokens = len(self.tokenizer(prompt_text)) + prompt_tokens = len(self.tokenizer(prompt_text)) # type: ignore[arg-type] max_tokens = req.max_completion_tokens or req.max_tokens or math.inf completion_tokens_count = min( sample_number(self.config.output_tokens, self.config.output_tokens_std), @@ -197,7 +197,7 @@ async def generate_stream(stream_response): # Token counts prompt_text = self.tokenizer.apply_chat_template(req.messages) - prompt_tokens = len(self.tokenizer(prompt_text)) + prompt_tokens = len(self.tokenizer(prompt_text)) # type: ignore[arg-type] max_tokens = req.max_completion_tokens or req.max_tokens or math.inf completion_tokens_count = int( min( diff --git a/src/guidellm/mock_server/utils.py b/src/guidellm/mock_server/utils.py index 8348d0a6..a839f484 100644 --- a/src/guidellm/mock_server/utils.py +++ b/src/guidellm/mock_server/utils.py @@ -58,12 +58,15 @@ def __call__(self, text: str | list[str], **kwargs) -> list[int]: # noqa: ARG00 return self.convert_tokens_to_ids(tokens) elif isinstance(text, list): # Handle batch processing - return [self.__call__(t) for t in text] + result = [] + for t in text: + result.extend(self.__call__(t)) + return result else: msg = f"text input must be of type `str` or `list[str]`, got {type(text)}" raise ValueError(msg) - def tokenize(self, text: TextInput, **_kwargs) -> list[str]: + def tokenize(self, text: TextInput, **_kwargs) -> list[str]: # type: ignore[override] """ Tokenize input text into a list of token strings. @@ -76,7 +79,7 @@ def tokenize(self, text: TextInput, **_kwargs) -> list[str]: # Split text into tokens: words, spaces, and punctuation return re.findall(r"\w+|[^\w\s]|\s+", text) - def convert_tokens_to_ids(self, tokens: str | list[str]) -> int | list[int]: + def convert_tokens_to_ids(self, tokens: str | list[str]) -> list[int]: """ Convert token strings to numeric token IDs. @@ -87,12 +90,12 @@ def convert_tokens_to_ids(self, tokens: str | list[str]) -> int | list[int]: :return: Single token ID or list of token IDs """ if isinstance(tokens, str): - return hash(tokens) % self.VocabSize + return [hash(tokens) % self.VocabSize] return [hash(token) % self.VocabSize for token in tokens] - def convert_ids_to_tokens( - self, ids: int | list[int], _skip_special_tokens: bool = False - ) -> str | list[str]: + def convert_ids_to_tokens( # type: ignore[override] + self, ids: list[int], _skip_special_tokens: bool = False + ) -> list[str]: """ Convert numeric token IDs back to token strings. @@ -102,17 +105,9 @@ def convert_ids_to_tokens( :param ids: Single token ID or list of token IDs to convert :return: Single token string or list of token strings """ - if not ids and not isinstance(ids, list): - return "" - elif not ids: + if not ids: return [""] - if isinstance(ids, int): - fake = Faker() - fake.seed_instance(ids % self.VocabSize) - - return fake.word() - fake = Faker() fake.seed_instance(sum(ids) % self.VocabSize) @@ -162,7 +157,7 @@ def _add_tokens( """ return 0 - def apply_chat_template( + def apply_chat_template( # type: ignore[override] self, conversation: list, tokenize: bool = False, # Changed default to False to match transformers @@ -193,7 +188,7 @@ def apply_chat_template( return self.convert_tokens_to_ids(self.tokenize(formatted_text)) return formatted_text - def decode( + def decode( # type: ignore[override] self, token_ids: list[int], skip_special_tokens: bool = True, @@ -255,7 +250,7 @@ def create_fake_tokens_str( fake = Faker() fake.seed_instance(seed) - tokens = [] + tokens: list[str] = [] while len(tokens) < num_tokens: text = fake.text( diff --git a/src/guidellm/preprocess/dataset.py b/src/guidellm/preprocess/dataset.py index a94b8a14..cacce3f5 100644 --- a/src/guidellm/preprocess/dataset.py +++ b/src/guidellm/preprocess/dataset.py @@ -1,9 +1,9 @@ import json import os -from collections.abc import Iterator +from collections.abc import Callable, Iterator from enum import Enum from pathlib import Path -from typing import Any, Callable, Optional, Union +from typing import Any import yaml from datasets import Dataset @@ -11,7 +11,6 @@ from pydantic import BaseModel, Field from transformers import PreTrainedTokenizerBase -from guidellm.dataset import load_dataset as guidellm_load_dataset from guidellm.utils import IntegerRangeSampler, check_load_processor from guidellm.utils.hf_datasets import SUPPORTED_TYPES, save_dataset_to_file @@ -32,7 +31,7 @@ def handle_ignore_strategy( min_prompt_tokens: int, tokenizer: PreTrainedTokenizerBase, **_kwargs, -) -> Optional[str]: +) -> str | None: """ Ignores prompts that are shorter than the required minimum token length. @@ -56,7 +55,7 @@ def handle_concatenate_strategy( tokenizer: PreTrainedTokenizerBase, concat_delimiter: str, **_kwargs, -) -> Optional[str]: +) -> str | None: """ Concatenates prompts until the minimum token requirement is met. @@ -117,7 +116,7 @@ def handle_error_strategy( min_prompt_tokens: int, tokenizer: PreTrainedTokenizerBase, **_kwargs, -) -> Optional[str]: +) -> str | None: """ Raises an error if the prompt is too short. @@ -150,24 +149,24 @@ class TokensConfig(BaseModel): description="The average number of tokens.", gt=0, ) - stdev: Optional[int] = Field( + stdev: int | None = Field( description="The standard deviation of the tokens.", gt=0, default=None, ) - min: Optional[int] = Field( + min: int | None = Field( description="The minimum number of tokens.", gt=0, default=None, ) - max: Optional[int] = Field( + max: int | None = Field( description="The maximum number of tokens.", gt=0, default=None, ) @staticmethod - def parse_str(data: Union[str, Path]) -> "TokensConfig": + def parse_str(data: str | Path) -> "TokensConfig": """ Parses a string or path into a TokensConfig object. Supports: - JSON string @@ -215,14 +214,14 @@ def parse_key_value_pairs(data: str) -> "TokensConfig": return TokensConfig(**config_dict) # type: ignore[arg-type] @staticmethod - def parse_config_file(data: Union[str, Path]) -> "TokensConfig": + def parse_config_file(data: str | Path) -> "TokensConfig": with Path(data).open("r") as file: config_dict = yaml.safe_load(file) return TokensConfig(**config_dict) -def _validate_output_suffix(output_path: Union[str, Path]) -> None: +def _validate_output_suffix(output_path: str | Path) -> None: output_path = Path(output_path) suffix = output_path.suffix.lower() if suffix not in SUPPORTED_TYPES: @@ -233,18 +232,18 @@ def _validate_output_suffix(output_path: Union[str, Path]) -> None: def process_dataset( - data: Union[str, Path], - output_path: Union[str, Path], - processor: Union[str, Path, PreTrainedTokenizerBase], - prompt_tokens: Union[str, Path], - output_tokens: Union[str, Path], - processor_args: Optional[dict[str, Any]] = None, - data_args: Optional[dict[str, Any]] = None, + data: str | Path, + output_path: str | Path, + processor: str | Path | PreTrainedTokenizerBase, + prompt_tokens: str | Path, + output_tokens: str | Path, + processor_args: dict[str, Any] | None = None, + data_args: dict[str, Any] | None = None, short_prompt_strategy: ShortPromptStrategy = ShortPromptStrategy.IGNORE, - pad_char: Optional[str] = None, - concat_delimiter: Optional[str] = None, + pad_char: str | None = None, + concat_delimiter: str | None = None, push_to_hub: bool = False, - hub_dataset_id: Optional[str] = None, + hub_dataset_id: str | None = None, random_seed: int = 42, ) -> None: """ @@ -271,9 +270,7 @@ def process_dataset( f"Starting dataset conversion | Input: {data} | Output directory: {output_path}" ) - dataset, column_mappings = guidellm_load_dataset( - data, data_args, processor, processor_args - ) + dataset, column_mappings = None, None tokenizer = check_load_processor( processor, processor_args, @@ -354,7 +351,7 @@ def process_dataset( def push_dataset_to_hub( - hub_dataset_id: Optional[str], + hub_dataset_id: str | None, processed_dataset: Dataset, ) -> None: """ diff --git a/src/guidellm/presentation/data_models.py b/src/guidellm/presentation/data_models.py index c1e8f13f..deec925c 100644 --- a/src/guidellm/presentation/data_models.py +++ b/src/guidellm/presentation/data_models.py @@ -1,7 +1,7 @@ import random from collections import defaultdict from math import ceil -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from pydantic import BaseModel, computed_field @@ -12,14 +12,14 @@ class Bucket(BaseModel): - value: Union[float, int] + value: float | int count: int @staticmethod def from_data( - data: Union[list[float], list[int]], - bucket_width: Optional[float] = None, - n_buckets: Optional[int] = None, + data: list[float] | list[int], + bucket_width: float | None = None, + n_buckets: int | None = None, ) -> tuple[list["Bucket"], float]: if not data: return [], 1.0 @@ -35,7 +35,7 @@ def from_data( else: n_buckets = ceil(range_v / bucket_width) - bucket_counts: defaultdict[Union[float, int], int] = defaultdict(int) + bucket_counts: defaultdict[float | int, int] = defaultdict(int) for val in data: idx = int((val - min_v) // bucket_width) if idx >= n_buckets: @@ -72,7 +72,7 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): bm.run_stats.start_time for bm in benchmarks if bm.start_time is not None ) return cls( - model=Model(name=model, size=0), + model=Model(name=model or "", size=0), task="N/A", timestamp=timestamp, dataset=Dataset(name="N/A"), @@ -80,7 +80,7 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): class Distribution(BaseModel): - statistics: Optional[DistributionSummary] = None + statistics: DistributionSummary | None = None buckets: list[Bucket] bucket_width: float @@ -117,21 +117,25 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): range(len(successful_requests)), min(5, len(successful_requests)) ) sample_prompts = [ - successful_requests[i].prompt.replace("\n", " ").replace('"', "'") + req.request_args.replace("\n", " ").replace('"', "'") + if (req := successful_requests[i]).request_args + else "" for i in sample_indices ] sample_outputs = [ - successful_requests[i].output.replace("\n", " ").replace('"', "'") + req.output.replace("\n", " ").replace('"', "'") + if (req := successful_requests[i]).output + else "" for i in sample_indices ] prompt_tokens = [ - float(req.prompt_tokens) + float(req.prompt_tokens) if req.prompt_tokens is not None else -1 for bm in benchmarks for req in bm.requests.successful ] output_tokens = [ - float(req.output_tokens) + float(req.output_tokens) if req.output_tokens is not None else -1 for bm in benchmarks for req in bm.requests.successful ] @@ -155,10 +159,10 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): min_start_time = benchmarks[0].start_time all_req_times = [ - req.scheduler_info.started_at - min_start_time + req.info.timings.request_start - min_start_time for bm in benchmarks for req in bm.requests.successful - if req.scheduler_info.started_at is not None + if req.info.timings.request_start is not None ] number_of_buckets = len(benchmarks) request_over_time_buckets, bucket_width = Bucket.from_data( @@ -190,7 +194,7 @@ class TabularDistributionSummary(DistributionSummary): """ @computed_field - def percentile_rows(self) -> list[dict[str, Union[str, float]]]: + def percentile_rows(self) -> list[dict[str, str | float]]: rows = [ {"percentile": name, "value": value} for name, value in self.percentiles.model_dump().items() @@ -208,7 +212,7 @@ def from_distribution_summary( class BenchmarkDatum(BaseModel): requests_per_second: float - tpot: TabularDistributionSummary + itl: TabularDistributionSummary ttft: TabularDistributionSummary throughput: TabularDistributionSummary time_per_request: TabularDistributionSummary @@ -217,7 +221,7 @@ class BenchmarkDatum(BaseModel): def from_benchmark(cls, bm: "GenerativeBenchmark"): return cls( requests_per_second=bm.metrics.requests_per_second.successful.mean, - tpot=TabularDistributionSummary.from_distribution_summary( + itl=TabularDistributionSummary.from_distribution_summary( bm.metrics.inter_token_latency_ms.successful ), ttft=TabularDistributionSummary.from_distribution_summary( diff --git a/src/guidellm/presentation/injector.py b/src/guidellm/presentation/injector.py index bb1fd684..1e78080e 100644 --- a/src/guidellm/presentation/injector.py +++ b/src/guidellm/presentation/injector.py @@ -1,6 +1,5 @@ import re from pathlib import Path -from typing import Union from loguru import logger @@ -8,7 +7,7 @@ from guidellm.utils.text import load_text -def create_report(js_data: dict, output_path: Union[str, Path]) -> Path: +def create_report(js_data: dict, output_path: str | Path) -> Path: """ Creates a report from the dictionary and saves it to the output path. diff --git a/src/guidellm/request/__init__.py b/src/guidellm/request/__init__.py deleted file mode 100644 index 85b447d6..00000000 --- a/src/guidellm/request/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from .loader import ( - GenerativeRequestLoader, - GenerativeRequestLoaderDescription, - RequestLoader, - RequestLoaderDescription, -) -from .request import GenerationRequest -from .types import RequestT, ResponseT - -__all__ = [ - "GenerationRequest", - "GenerativeRequestLoader", - "GenerativeRequestLoaderDescription", - "RequestLoader", - "RequestLoaderDescription", - "RequestT", - "ResponseT", -] diff --git a/src/guidellm/request/loader.py b/src/guidellm/request/loader.py deleted file mode 100644 index 607a7455..00000000 --- a/src/guidellm/request/loader.py +++ /dev/null @@ -1,284 +0,0 @@ -from abc import abstractmethod -from collections.abc import Iterable, Iterator -from pathlib import Path -from typing import ( - Any, - Literal, - Optional, - Union, -) - -from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset import ColumnInputTypes, load_dataset -from guidellm.request.request import GenerationRequest -from guidellm.settings import settings -from guidellm.utils import StandardBaseModel - -__all__ = [ - "GenerativeRequestLoader", - "GenerativeRequestLoaderDescription", - "RequestLoader", - "RequestLoaderDescription", -] - - -class RequestLoaderDescription(StandardBaseModel): - type_: Literal["request_loader"] = "request_loader" - - -class RequestLoader(Iterable): - @abstractmethod - def __iter__(self) -> Iterator: ... - - @abstractmethod - def __len__(self) -> int: ... - - @property - @abstractmethod - def description(self) -> RequestLoaderDescription: ... - - -class GenerativeRequestLoaderDescription(RequestLoaderDescription): - type_: Literal["generative_request_loader"] = "generative_request_loader" # type: ignore[assignment] - data: str - data_args: Optional[dict[str, Any]] - processor: str - processor_args: Optional[dict[str, Any]] - - -class GenerativeRequestLoader(RequestLoader): - DEFAULT_PROMPT_COLUMNS = [ - "prompt", - "prompts", - "instruction", - "instructions", - "question", - "questions", - "input", - "inputs", - "context", - "content", - "conversation", - "conversations", - "turn", - "turns", - "text", - ] - - def __init__( - self, - data: Union[ - str, - Path, - Iterable[Union[str, dict[str, Any]]], - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, - ], - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - shuffle: bool = True, - iter_type: Literal["finite", "infinite"] = "finite", - random_seed: int = 42, - ): - self.data = data - self.data_args = data_args - dataset, args_column_mappings = load_dataset( - data, - data_args, - processor, - processor_args, - random_seed, - ) - self.dataset = dataset - self.processor = processor - self.processor_args = processor_args - self.shuffle = shuffle - self.iter_type = iter_type - self.random_seed = random_seed - - self.column_mappings = self._create_column_mappings(args_column_mappings) - self.preserve_iter_state = iter_type == "infinite" # ensure no caching requests - self._preserved_iter = None - - def __iter__(self) -> Iterator[GenerationRequest]: - scope_create_count = 0 - - while (dataset_iter := self._get_dataset_iter(scope_create_count)) is not None: - scope_create_count += 1 - - for item in dataset_iter: - yield self._create_request(item) - - self._preserved_iter = None - - def __len__(self) -> int: - if self.iter_type == "finite": - return self.num_unique_items() - - raise ValueError(f"Unable to determine length of dataset: {self.data}") - - @property - def description(self) -> GenerativeRequestLoaderDescription: - return GenerativeRequestLoaderDescription( - data=str(self.data), - data_args=self.data_args, - processor=str(self.processor), - processor_args=self.processor_args, - ) - - def num_unique_items(self, raise_err: bool = True) -> int: - try: - return len(self.dataset) - except Exception: # noqa: BLE001, S110 - pass - - dataset_size = self.dataset.info.dataset_size - if dataset_size is not None: - return dataset_size - - if raise_err: - raise ValueError("Unable to determine number of items in the dataset") - - return -1 - - def _create_column_mappings( - self, - args_column_mappings: dict[ColumnInputTypes, str], - ) -> dict[ColumnInputTypes, str]: - column_mappings: dict[ColumnInputTypes, str] = {} - - if "text_column" in args_column_mappings: - column_mappings["prompt_column"] = args_column_mappings["text_column"] - else: - column_mappings["prompt_column"] = self._extract_text_column() - - if "prompt_tokens_count_column" in args_column_mappings: - column_mappings["prompt_tokens_count_column"] = args_column_mappings[ - "prompt_tokens_count_column" - ] - elif prompt_tokens_count_column := self._extract_prompt_tokens_count_column(): - column_mappings["prompt_tokens_count_column"] = prompt_tokens_count_column - - if "output_tokens_count_column" in args_column_mappings: - column_mappings["output_tokens_count_column"] = args_column_mappings[ - "output_tokens_count_column" - ] - elif output_tokens_count_column := self._extract_output_tokens_count_column(): - column_mappings["output_tokens_count_column"] = output_tokens_count_column - - return column_mappings - - def _extract_text_column(self) -> str: - column_names = self._dataset_columns( - err_msg=( - "Unable to determine text column from dataset and it is required. " - "To specify the text column, set the 'text_column' key in the " - "'data_args' dictionary." - ) - ) - - if not column_names: - raise ValueError( - "Unable to determine text column from dataset and it is required. " - "To specify the text column, set the 'text_column' key in the " - "'data_args' dictionary." - ) - - if len(column_names) == 1: - return column_names[0] - - for def_column in self.DEFAULT_PROMPT_COLUMNS: - if def_column in column_names: - return def_column - - raise ValueError( - f"Unable to determine text column from dataset columns: {column_names}. " - "To specify the text column, set the 'text_column' key in the " - "'data_args' dictionary." - ) - - def _extract_prompt_tokens_count_column(self) -> Optional[str]: - column_names = self._dataset_columns() - - if column_names and "prompt_tokens_count" in column_names: - return "prompt_tokens_count" - - if column_names and "prompt_tokens" in column_names: - return "prompt_tokens" - - return None - - def _extract_output_tokens_count_column(self) -> Optional[str]: - column_names = self._dataset_columns() - - if column_names and "output_tokens_count" in column_names: - return "output_tokens_count" - - if column_names and "output_tokens" in column_names: - return "output_tokens" - - return None - - def _dataset_columns(self, err_msg: Optional[str] = None) -> Optional[list[str]]: - try: - column_names = self.dataset.column_names - - if not column_names and err_msg: - raise ValueError(f"No column names found in dataset: {self.data}") - except Exception as err: - if err_msg: - raise ValueError(err_msg) from err - - column_names = None - - return column_names - - def _get_dataset_iter( - self, scope_create_count: int - ) -> Optional[Iterator[dict[str, Any]]]: - if scope_create_count > 0 and self.iter_type != "infinite": - return None - - if self.preserve_iter_state and self._preserved_iter is not None: - return self._preserved_iter - - dataset = ( - self.dataset - if not self.shuffle - else self.dataset.shuffle(seed=self.random_seed) - ) - - dataset_iter = iter(dataset) - - if self.preserve_iter_state: - self._preserved_iter = dataset_iter - - return dataset_iter - - def _create_request(self, item: dict[str, Any]) -> GenerationRequest: - prompt_tokens = ( - item[self.column_mappings["prompt_tokens_count_column"]] - if "prompt_tokens_count_column" in self.column_mappings - else None - ) - output_tokens = ( - item[self.column_mappings["output_tokens_count_column"]] - if "output_tokens_count_column" in self.column_mappings - else None - ) - - return GenerationRequest( - request_type=settings.preferred_route, - content=item[self.column_mappings["prompt_column"]], - stats=( - {"prompt_tokens": prompt_tokens} if prompt_tokens is not None else {} - ), - constraints=( - {"output_tokens": output_tokens} if output_tokens is not None else {} - ), - ) diff --git a/src/guidellm/request/request.py b/src/guidellm/request/request.py deleted file mode 100644 index bf4e59fb..00000000 --- a/src/guidellm/request/request.py +++ /dev/null @@ -1,79 +0,0 @@ -import uuid -from typing import Any, Literal, Optional - -from pydantic import Field - -from guidellm.utils import StandardBaseModel - -__all__ = ["GenerationRequest"] - - -class GenerationRequest(StandardBaseModel): - """ - A class representing a request for generation. - This class is used to encapsulate the details of a generation request, - including the request ID, type, content, parameters, statistics, and constraints. - It is designed to be used with the BackendRequestsWorker class to handle - the generation process. - - :param request_id: The unique identifier for the request. - :param request_type: The type of request (e.g., text, chat). - :param content: The content for the request to send to the backend. - If request_type is 'text', this should be a string or list of strings - which will be resolved by backend.text_completions. - If request_type is 'chat', this should be a string, - a list of (str, Dict[str, Union[str, Dict[str, str]], Path, Image]), - or Any raw content which will be resolved by backend.chat_completions. - If raw content, raw_content=True must be passed in the params. - :param params: Additional parameters for the request passed in as kwargs. - For an http backend, these are passed into the body of the request. - :param stats: Statistics for the request, such as the number of prompt tokens. - Used for tracking and reporting purposes. - :param constraints: Constraints for the request, such as the maximum number - of output tokens. Used for controlling the behavior of the backend. - """ - - request_id: Optional[str] = Field( - default_factory=lambda: str(uuid.uuid4()), - description="The unique identifier for the request.", - ) - request_type: Literal["text_completions", "chat_completions"] = Field( - default="text_completions", - description=( - "The type of request (e.g., text, chat). " - "If request_type='text_completions', resolved by backend.text_completions. " - "If request_typ='chat_completions', resolved by backend.chat_completions." - ), - ) - content: Any = Field( - description=( - "The content for the request to send to the backend. " - "If request_type is 'text', this should be a string or list of strings " - "which will be resolved by backend.text_completions. " - "If request_type is 'chat', this should be a string, " - "a list of (str, Dict[str, Union[str, Dict[str, str]], Path, Image]), " - "or Any raw content which will be resolved by backend.chat_completions. " - "If raw content, raw_content=True must be passed in the params." - ) - ) - params: dict[str, Any] = Field( - default_factory=dict, - description=( - "Additional parameters for the request that will be passed in as kwargs. " - "For an http backend, these are passed into the body of the request. " - ), - ) - stats: dict[Literal["prompt_tokens"], int] = Field( - default_factory=dict, - description=( - "Statistics for the request, such as the number of prompt tokens. " - "Used for tracking and reporting purposes." - ), - ) - constraints: dict[Literal["output_tokens"], int] = Field( - default_factory=dict, - description=( - "Constraints for the request, such as the maximum number of output tokens. " - "Used for controlling the behavior of the backend." - ), - ) diff --git a/src/guidellm/request/types.py b/src/guidellm/request/types.py deleted file mode 100644 index f82493be..00000000 --- a/src/guidellm/request/types.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import TypeVar - -__all__ = [ - "RequestT", - "ResponseT", -] - - -RequestT = TypeVar("RequestT") -ResponseT = TypeVar("ResponseT") diff --git a/src/guidellm/scheduler/__init__.py b/src/guidellm/scheduler/__init__.py index 64647424..9b74c44a 100644 --- a/src/guidellm/scheduler/__init__.py +++ b/src/guidellm/scheduler/__init__.py @@ -1,3 +1,15 @@ +""" +Scheduler subsystem for orchestrating benchmark workloads and managing worker processes. + +This module provides the core scheduling infrastructure for guidellm, including +strategies for controlling request timing patterns (synchronous, asynchronous, +constant rate, Poisson), constraints for limiting benchmark execution (duration, +error rates, request counts), and distributed execution through worker processes. +The scheduler coordinates between backend interfaces, manages benchmark state +transitions, and handles multi-turn request sequences with customizable timing +strategies and resource constraints. +""" + from .constraints import ( Constraint, ConstraintInitializer, @@ -12,30 +24,22 @@ UnserializableConstraintInitializer, ) from .environments import Environment, NonDistributedEnvironment -from .objects import ( +from .scheduler import Scheduler +from .schemas import ( BackendInterface, BackendT, - MeasuredRequestTimings, MultiTurnRequestT, - RequestSchedulerTimings, RequestT, ResponseT, - ScheduledRequestInfo, SchedulerMessagingPydanticRegistry, SchedulerState, SchedulerUpdateAction, SchedulerUpdateActionProgress, ) -from .scheduler import Scheduler from .strategies import ( AsyncConstantStrategy, AsyncPoissonStrategy, ConcurrentStrategy, - ConstantRateRequestTimings, - LastCompletionRequestTimings, - NoDelayRequestTimings, - PoissonRateRequestTimings, - ScheduledRequestTimings, SchedulingStrategy, StrategyT, StrategyType, @@ -62,16 +66,13 @@ "MaxErrorsConstraint", "MaxGlobalErrorRateConstraint", "MaxNumberConstraint", - "MeasuredRequestTimings", "MultiTurnRequestT", "NoDelayRequestTimings", "NonDistributedEnvironment", "PoissonRateRequestTimings", "PydanticConstraintInitializer", - "RequestSchedulerTimings", "RequestT", "ResponseT", - "ScheduledRequestInfo", "ScheduledRequestTimings", "Scheduler", "SchedulerMessagingPydanticRegistry", diff --git a/src/guidellm/scheduler/constraints.py b/src/guidellm/scheduler/constraints.py index c724a74a..2eb24bdb 100644 --- a/src/guidellm/scheduler/constraints.py +++ b/src/guidellm/scheduler/constraints.py @@ -16,12 +16,12 @@ from pydantic import Field, field_validator -from guidellm.scheduler.objects import ( - ScheduledRequestInfo, +from guidellm.scheduler.schemas import ( SchedulerState, SchedulerUpdateAction, SchedulerUpdateActionProgress, ) +from guidellm.schemas import RequestInfo from guidellm.settings import settings from guidellm.utils import InfoMixin, RegistryMixin, StandardBaseModel @@ -46,7 +46,7 @@ class Constraint(Protocol): """Protocol for constraint evaluation functions that control scheduler behavior.""" def __call__( - self, state: SchedulerState, request: ScheduledRequestInfo + self, state: SchedulerState, request: RequestInfo ) -> SchedulerUpdateAction: """ Evaluate constraint against scheduler state and request information. @@ -176,7 +176,7 @@ def serialize(cls, initializer: ConstraintInitializer) -> dict[str, Any]: @classmethod def deserialize( cls, initializer_dict: dict[str, Any] - ) -> SerializableConstraintInitializer: + ) -> SerializableConstraintInitializer | UnserializableConstraintInitializer: """ Deserialize constraint initializer from dictionary format. @@ -370,7 +370,7 @@ def create_constraint( def __call__( self, state: SchedulerState, # noqa: ARG002 - request: ScheduledRequestInfo, # noqa: ARG002 + request: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Raise error since unserializable constraints cannot be invoked. @@ -438,7 +438,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Evaluate constraint against current scheduler state and request count. @@ -450,7 +450,7 @@ def __call__( current_index = max(0, self.current_index) max_num = ( self.max_num - if isinstance(self.max_num, (int, float)) + if isinstance(self.max_num, int | float) else self.max_num[min(current_index, len(self.max_num) - 1)] ) @@ -489,7 +489,7 @@ def _validate_max_num( raise ValueError( f"max_num must be set and truthful, received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0: + if not isinstance(val, int | float) or val <= 0: raise ValueError( f"max_num must be a positive num, received {value} ({val} failed)" ) @@ -556,7 +556,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Evaluate constraint against current scheduler state and elapsed time. @@ -568,7 +568,7 @@ def __call__( current_index = max(0, self.current_index) max_duration = ( self.max_duration - if isinstance(self.max_duration, (int, float)) + if isinstance(self.max_duration, int | float) else self.max_duration[min(current_index, len(self.max_duration) - 1)] ) @@ -607,7 +607,7 @@ def _validate_max_duration( "max_duration must be set and truthful, " f"received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0: + if not isinstance(val, int | float) or val <= 0: raise ValueError( "max_duration must be a positive num," f"received {value} ({val} failed)" @@ -670,7 +670,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Evaluate constraint against current error count. @@ -682,7 +682,7 @@ def __call__( current_index = max(0, self.current_index) max_errors = ( self.max_errors - if isinstance(self.max_errors, (int, float)) + if isinstance(self.max_errors, int | float) else self.max_errors[min(current_index, len(self.max_errors) - 1)] ) errors_exceeded = state.errored_requests >= max_errors @@ -710,7 +710,7 @@ def _validate_max_errors( "max_errors must be set and truthful, " f"received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0: + if not isinstance(val, int | float) or val <= 0: raise ValueError( f"max_errors must be a positive num,received {value} ({val} failed)" ) @@ -787,7 +787,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 return self.model_copy() # type: ignore[return-value] def __call__( - self, state: SchedulerState, request_info: ScheduledRequestInfo + self, state: SchedulerState, request_info: RequestInfo ) -> SchedulerUpdateAction: """ Evaluate constraint against sliding window error rate. @@ -799,7 +799,7 @@ def __call__( current_index = max(0, self.current_index) max_error_rate = ( self.max_error_rate - if isinstance(self.max_error_rate, (int, float)) + if isinstance(self.max_error_rate, int | float) else self.max_error_rate[min(current_index, len(self.max_error_rate) - 1)] ) @@ -850,7 +850,7 @@ def _validate_max_error_rate( "max_error_rate must be set and truthful, " f"received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0 or val >= 1: + if not isinstance(val, int | float) or val <= 0 or val >= 1: raise ValueError( "max_error_rate must be a number between 0 and 1," f"received {value} ({val} failed)" @@ -928,7 +928,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Evaluate constraint against global error rate. @@ -940,7 +940,7 @@ def __call__( current_index = max(0, self.current_index) max_error_rate = ( self.max_error_rate - if isinstance(self.max_error_rate, (int, float)) + if isinstance(self.max_error_rate, int | float) else self.max_error_rate[min(current_index, len(self.max_error_rate) - 1)] ) @@ -982,7 +982,7 @@ def _validate_max_error_rate( "max_error_rate must be set and truthful, " f"received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0 or val >= 1: + if not isinstance(val, int | float) or val <= 0 or val >= 1: raise ValueError( "max_error_rate must be a number between 0 and 1," f"received {value} ({val} failed)" @@ -1007,7 +1007,7 @@ def info(self) -> dict[str, Any]: def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: create_exceeded = state.created_requests >= self.num_requests processed_exceeded = state.processed_requests >= self.num_requests diff --git a/src/guidellm/scheduler/environments.py b/src/guidellm/scheduler/environments.py index 6234f8f6..4f02d772 100644 --- a/src/guidellm/scheduler/environments.py +++ b/src/guidellm/scheduler/environments.py @@ -1,18 +1,19 @@ """ Environment abstractions for coordinating scheduler execution across distributed nodes. -Provides environment abstractions that handle synchronization, timing coordination, -error propagation, and lifecycle management for scheduler execution across single -or multiple nodes. The Environment protocol defines the interface for distributed +Provides abstractions that handle synchronization, timing coordination, error +propagation, and lifecycle management for scheduler execution across single or +multiple nodes. The Environment protocol defines the interface for distributed coordination while NonDistributedEnvironment provides a minimal implementation -for single-node execution. +for single-node execution. Environments manage the complete execution lifecycle +from parameter distribution through result aggregation. -Environment Execution Flow: -1. sync_run_params() - Distribute workload and synchronize parameters across nodes -2. sync_run_start() - Coordinate synchronized start time for all nodes -3. update_run_iteration() - Update state after each request (called per iteration) +Execution Flow: +1. sync_run_params() - Distribute workload and synchronize parameters +2. sync_run_start() - Coordinate synchronized start time +3. update_run_iteration() - Update state after each request iteration 4. sync_run_error() - Handle and propagate errors across nodes -5. sync_run_end() - Aggregate results and cleanup at completion +5. sync_run_end() - Aggregate results and finalize execution """ from __future__ import annotations @@ -20,19 +21,17 @@ import time from abc import ABC, abstractmethod from collections.abc import AsyncIterator, Iterable -from typing import ( - Generic, -) +from typing import Generic from guidellm.scheduler.constraints import Constraint -from guidellm.scheduler.objects import ( +from guidellm.scheduler.schemas import ( MultiTurnRequestT, RequestT, ResponseT, - ScheduledRequestInfo, SchedulerState, ) from guidellm.scheduler.strategies import SchedulingStrategy +from guidellm.schemas import RequestInfo from guidellm.settings import settings from guidellm.utils import InfoMixin @@ -41,12 +40,12 @@ class Environment(ABC, Generic[RequestT, ResponseT], InfoMixin): """ - Abstract base for coordinating scheduler execution across distributed nodes. + Abstract interface for coordinating scheduler execution across distributed nodes. - Defines the interface for managing distributed scheduler execution including + Defines the protocol for managing distributed scheduler execution including parameter synchronization, timing coordination, state updates, error propagation, - and result aggregation. Implementations handle the complexity of distributed - coordination while providing a unified interface for scheduler orchestration. + and result aggregation. Implementations handle distributed coordination complexity + while providing a unified interface for scheduler orchestration. """ @abstractmethod @@ -63,10 +62,6 @@ async def sync_run_params( """ Synchronize execution parameters across nodes and resolve local scope. - Coordinates parameter distribution and validation across active nodes. - In distributed environments, handles node assignment and workload partitioning. - In non-distributed environments, typically returns parameters unchanged. - :param requests: Complete set of requests to process across all nodes :param strategy: Scheduling strategy to apply during execution :param constraints: Runtime constraints to enforce during execution @@ -80,9 +75,6 @@ async def sync_run_start(self) -> float: """ Coordinate synchronized start time across all nodes. - Ensures all nodes begin processing simultaneously for accurate benchmarking - and consistent timing measurements across distributed execution. - :return: Unix timestamp when all nodes should begin processing :raises Exception: If startup synchronization fails across nodes """ @@ -93,17 +85,12 @@ async def update_run_iteration( self, response: ResponseT | None, request: RequestT, - request_info: ScheduledRequestInfo, + request_info: RequestInfo, state: SchedulerState, ): """ Update environment state with completed request iteration results. - Called after each request processing to update execution progress and - synchronize any required state across nodes in distributed environments. - Generally, distributed is expected to store the iteration updates until - all nodes have processed and sync_run_end is called to retrieve them. - :param response: Response generated for the request, if successful :param request: The processed request :param request_info: Metadata about request processing including timings @@ -117,9 +104,6 @@ async def sync_run_error(self, err: list[Exception] | Exception): """ Handle and propagate errors across all active nodes. - Coordinates error handling when failures occur, ensuring all nodes are - notified for appropriate cleanup or shutdown procedures. - :param err: The exception(s) that occurred during execution """ ... @@ -129,67 +113,61 @@ async def sync_run_end( self, ) -> AsyncIterator[ tuple[ - ResponseT, - RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, + ResponseT | None, + RequestT, + RequestInfo, SchedulerState, ] ]: """ Finalize execution and aggregate results from all nodes. - Handles cleanup, result synchronization, and error propagation at execution - completion. Collects and yields results from worker nodes in distributed - environments. - :return: Iterator of (response, request, request_info, state) tuples from remote nodes in distributed environments, empty for non-distributed :raises Exception: Any errors that occurred during execution """ - ... + yield None # type: ignore[misc] -class NonDistributedEnvironment(Environment): +class NonDistributedEnvironment(Environment[RequestT, ResponseT]): """ Single-node scheduler execution environment with minimal coordination overhead. - Simplified environment for running schedulers on a single node without distributed - coordination requirements. Implements the Environment interface with no-op - synchronization for local testing, development, and single-machine benchmarking. + Implements the Environment interface with no-op synchronization for local testing, + development, and single-machine benchmarking. All synchronization methods return + immediately without distributed coordination logic. Example: :: from guidellm.scheduler import ( MaxNumberConstraint, NonDistributedEnvironment, - ScheduledRequestInfo, + RequestInfo, SchedulerState, SynchronousStrategy, ) - - # Definitions + env = NonDistributedEnvironment() requests = [f"req_{ind}" for ind in range(5)] strategy = SynchronousStrategy() constraints = {"max_num": MaxNumberConstraint(max_num=5)} state = SchedulerState() - # Run environment local_req, local_strat, local_const = await env.sync_run_params( requests, strategy, constraints ) start_time = await env.sync_run_start() for req in local_req: state.processed_requests += 1 - await env.update_run_iteration( - f"resp_{req}", req, ScheduledRequestInfo(), state - ) + await env.update_run_iteration(f"resp_{req}", req, RequestInfo(), state) async for nonlocal_req in env.sync_run_end(): state.processed_requests += 1 """ def __init__(self): - """Initialize with empty error storage for single-node execution.""" + """ + Initialize single-node environment with empty error storage. + """ self.run_errors: list[Exception] = [] async def sync_run_params( @@ -208,7 +186,7 @@ async def sync_run_params( :param requests: Requests to process locally :param strategy: Scheduling strategy to apply during execution :param constraints: Runtime constraints to enforce during execution - :return: Tuple containing the original (requests, strategy, constraints) + :return: Original (requests, strategy, constraints) tuple unchanged """ return requests, strategy, constraints @@ -216,7 +194,7 @@ async def sync_run_start(self) -> float: """ Return current time plus configured delay for single-node startup. - :return: Unix timestamp for when the run should start + :return: Unix timestamp when execution should begin """ return time.time() + settings.scheduler_start_delay_non_distributed @@ -224,19 +202,19 @@ async def update_run_iteration( self, response: ResponseT | None, request: RequestT, - request_info: ScheduledRequestInfo, + request_info: RequestInfo, state: SchedulerState, ): """ No-op for single-node execution with no distributed state synchronization. :param response: Response generated for the request, if successful - :param request: The request that was processed + :param request: The processed request :param request_info: Metadata about request processing including timings :param state: Current scheduler state with metrics and progress """ - async def sync_run_error(self, err: Exception): + async def sync_run_error(self, err: Exception | list[Exception]): """ Store error for later propagation during run finalization. @@ -249,16 +227,16 @@ async def sync_run_end( self, ) -> AsyncIterator[ tuple[ - ResponseT, - RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, + ResponseT | None, + RequestT, + RequestInfo, SchedulerState, ] ]: """ Finalize single-node execution and propagate any stored errors. - :return: Empty iterator since there are no remote nodes + :return: Empty iterator as there are no remote nodes :raises Exception: Any error stored during execution via sync_run_error """ if self.run_errors: @@ -269,5 +247,6 @@ async def sync_run_end( f"Errors occurred during execution: {self.run_errors}" ) - return - yield # needed to force generator compilation + if False: + # Force compiler to recognize as generator + yield None # type: ignore[misc] diff --git a/src/guidellm/scheduler/objects.py b/src/guidellm/scheduler/objects.py deleted file mode 100644 index 21d30ec8..00000000 --- a/src/guidellm/scheduler/objects.py +++ /dev/null @@ -1,470 +0,0 @@ -""" -Core data structures and interfaces for the GuideLLM scheduler system. - -Provides type-safe abstractions for distributed request processing, timing -measurements, and backend interfaces for benchmarking operations. Central to -the scheduler architecture, enabling request lifecycle tracking, backend -coordination, and state management across distributed worker processes. -""" - -from __future__ import annotations - -import time -import uuid -from collections.abc import AsyncIterator -from typing import ( - Any, - ClassVar, - Generic, - Literal, - Protocol, - TypeVar, - Union, - runtime_checkable, -) - -from pydantic import Field, computed_field -from typing_extensions import TypeAliasType, TypedDict - -from guidellm.utils import ( - PydanticClassRegistryMixin, - RegistryMixin, - StandardBaseModel, -) -from guidellm.utils.registry import RegistryObjT - -__all__ = [ - "BackendInterface", - "BackendT", - "MeasuredRequestTimings", - "MultiTurnRequestT", - "RequestSchedulerTimings", - "RequestT", - "ResponseT", - "ScheduledRequestInfo", - "SchedulerMessagingPydanticRegistry", - "SchedulerState", - "SchedulerUpdateAction", - "SchedulerUpdateActionProgress", -] - -RequestT = TypeVar("RequestT") -"""Generic request object type for scheduler processing.""" - -ResponseT = TypeVar("ResponseT") -"""Generic response object type returned by backend processing.""" - -MultiTurnRequestT = TypeAliasType( - "MultiTurnRequestT", - Union[ - list[Union[RequestT, tuple[RequestT, float]]], - tuple[Union[RequestT, tuple[RequestT, float]]], - ], - type_params=(RequestT,), -) -"""Multi-turn request structure supporting conversation history with optional delays.""" - - -class SchedulerMessagingPydanticRegistry(RegistryMixin[RegistryObjT]): - """ - Registry for enabling a generic interface to define the pydantic class types used - for inter-process messaging within the scheduler. - """ - - -@SchedulerMessagingPydanticRegistry.register() -class RequestSchedulerTimings(StandardBaseModel): - """ - Scheduler-level timing measurements for request lifecycle tracking. - All timestamps are expected to be in Unix time (seconds since epoch). - """ - - targeted_start: float | None = Field( - default=None, - description="When the request was initially targeted for execution", - ) - queued: float | None = Field( - default=None, - description="When the request was placed into the processing queue", - ) - dequeued: float | None = Field( - default=None, - description="When the request was removed from the queue for processing", - ) - scheduled_at: float | None = Field( - default=None, description="When the request was scheduled for processing" - ) - resolve_start: float | None = Field( - default=None, description="When backend resolution of the request began" - ) - resolve_end: float | None = Field( - default=None, description="When backend resolution of the request completed" - ) - finalized: float | None = Field( - default=None, - description="When the request was processed/acknowledged by the scheduler", - ) - - -@SchedulerMessagingPydanticRegistry.register() -class MeasuredRequestTimings(PydanticClassRegistryMixin["MeasuredRequestTimings"]): - """ - Base timing measurements for backend request processing. - All timestamps are expected to be in Unix time (seconds since epoch). - """ - - @classmethod - def __pydantic_schema_base_type__(cls) -> type[MeasuredRequestTimings]: - if cls.__name__ == "MeasuredRequestTimings": - return cls - - return MeasuredRequestTimings - - schema_discriminator: ClassVar[str] = "timings_type" - - timings_type: Literal["measured_request_timings"] = Field( - default="measured_request_timings", - description="Type identifier for the timing measurement", - ) - request_start: float | None = Field( - default=None, description="When the backend began processing the request" - ) - request_end: float | None = Field( - default=None, description="When the backend completed processing the request" - ) - - -@SchedulerMessagingPydanticRegistry.register() -class ScheduledRequestInfo(StandardBaseModel): - """ - Complete request information including status, timings, and metadata. - - Central data structure for tracking request lifecycle from creation through - completion, containing scheduling metadata, timing measurements, and processing - status. Used by scheduler components to coordinate request processing across - distributed worker processes. - - Example: - :: - from guidellm.scheduler.objects import ScheduledRequestInfo - - # Create request info with automatic ID generation - request_info = ScheduledRequestInfo() - request_info.status = "in_progress" - request_info.scheduler_timings.queued = time.time() - - # Check processing completion - if request_info.completed_at: - duration = request_info.completed_at - request_info.started_at - """ - - request_id: str = Field( - description="Unique identifier for the request", - default_factory=lambda: str(uuid.uuid4()), - ) - status: Literal[ - "queued", "pending", "in_progress", "completed", "errored", "cancelled" - ] = Field(description="Current processing status of the request", default="queued") - scheduler_node_id: int = Field( - description="ID/rank of the scheduler node handling the request", - default=-1, - ) - scheduler_process_id: int = Field( - description="ID/rank of the node's scheduler process handling the request", - default=-1, - ) - scheduler_start_time: float = Field( - description="Unix timestamp for the local time when scheduler processing began", - default=-1.0, - ) - - error: str | None = Field( - default=None, description="Error message if the request.status is 'errored'" - ) - scheduler_timings: RequestSchedulerTimings = Field( - default_factory=RequestSchedulerTimings, - description="Scheduler-level timing measurements for request lifecycle", - ) - request_timings: MeasuredRequestTimings | None = Field( - default=None, - description="Backend-specific timing measurements for request processing", - ) - - @computed_field # type: ignore[misc] - @property - def started_at(self) -> float | None: - """ - Get the effective request processing start time. - - :return: Unix timestamp when processing began, or None if not started. - """ - request_start = ( - self.request_timings.request_start if self.request_timings else None - ) - - return request_start or self.scheduler_timings.resolve_start - - @computed_field # type: ignore[misc] - @property - def completed_at(self) -> float | None: - """ - Get the effective request processing completion time. - - :return: Unix timestamp when processing completed, or None if not completed. - """ - request_end = self.request_timings.request_end if self.request_timings else None - - return request_end or self.scheduler_timings.resolve_end - - def model_copy(self, **kwargs) -> ScheduledRequestInfo: # type: ignore[override] # noqa: ARG002 - """ - Create a deep copy of the request info with copied timing objects. - - :return: New ScheduledRequestInfo instance with independent timing objects - """ - return super().model_copy( - update={ - "scheduler_timings": self.scheduler_timings.model_copy(), - "request_timings": ( - self.request_timings.model_copy() if self.request_timings else None - ), - }, - deep=False, - ) - - -@runtime_checkable -class BackendInterface(Protocol, Generic[RequestT, ResponseT]): - """ - Abstract interface for request processing backends. - - Defines the contract for backend implementations that process requests within - the scheduler system. Backends handle initialization, validation, processing, - and shutdown lifecycle management. Must ensure all properties are pickleable - before process_startup is invoked for multi-process environments. - - Example: - :: - from guidellm.scheduler.objects import BackendInterface - - class CustomBackend(BackendInterface): - @property - def processes_limit(self) -> int: - return 4 - - async def resolve(self, request, request_info, history=None): - # Process request and yield responses - yield response, updated_request_info - """ - - @property - def processes_limit(self) -> int | None: - """ - :return: Maximum worker processes supported, or None if unlimited - """ - - @property - def requests_limit(self) -> int | None: - """ - :return: Maximum concurrent requests supported, or None if unlimited - """ - - @property - def info(self) -> dict[str, Any]: - """ - :return: Backend metadata including model initialization and configuration - """ - - async def process_startup(self) -> None: - """ - Perform backend initialization and startup procedures. - - :raises: Implementation-specific exceptions for startup failures. - """ - - async def validate(self) -> None: - """ - Validate backend configuration and operational status. - - :raises: Implementation-specific exceptions for validation failures. - """ - - async def process_shutdown(self) -> None: - """ - Perform backend cleanup and shutdown procedures. - - :raises: Implementation-specific exceptions for shutdown failures. - """ - - async def resolve( - self, - request: RequestT, - request_info: ScheduledRequestInfo, - history: list[tuple[RequestT, ResponseT]] | None = None, - ) -> AsyncIterator[tuple[ResponseT, ScheduledRequestInfo]]: - """ - Process a request and yield incremental response updates. - - :param request: The request object to process - :param request_info: Scheduling metadata and timing information - :param history: Optional conversation history for multi-turn requests - :yield: Tuples of (response, updated_request_info) for each response chunk - :raises: Implementation-specific exceptions for processing failures - """ - - -BackendT = TypeVar("BackendT", bound=BackendInterface) -"""Generic backend interface type for request processing.""" - - -class SchedulerUpdateActionProgress(TypedDict, total=False): - """ - Progress information for a scheduler update action. - - Optional progress tracking data that provides estimates for remaining work - in scheduler operations. Used by constraints and monitoring systems to - track execution progress and make termination decisions. - """ - - remaining_fraction: float | None - remaining_requests: float | None - remaining_duration: float | None - - -class SchedulerUpdateAction(StandardBaseModel): - """ - Scheduler behavior control directives and actions. - - Encapsulates control signals for scheduler operations including request - queuing and processing directives. Used by constraints to communicate - termination conditions and progress information to scheduler components. - - Example: - :: - from guidellm.scheduler.objects import SchedulerUpdateAction - - # Signal to stop queuing but continue processing - action = SchedulerUpdateAction( - request_queuing="stop", - request_processing="continue", - metadata={"reason": "max_requests_reached"} - ) - """ - - request_queuing: Literal["continue", "stop"] = Field( - default="continue", description="Action to take for request queuing operations" - ) - request_processing: Literal["continue", "stop_local", "stop_all"] = Field( - default="continue", - description="Action to take for request processing operations", - ) - metadata: dict[str, Any] = Field( - default_factory=dict, - description="Additional context and data for the scheduler action", - ) - progress: SchedulerUpdateActionProgress = Field( - default_factory=lambda: SchedulerUpdateActionProgress(), - description="Progress information for the scheduler action", - ) - - -class SchedulerState(StandardBaseModel): - """ - Scheduler operation state tracking and statistics. - - Comprehensive state container for tracking scheduler execution progress, - request counts, timing information, and constraint enforcement. Central - to scheduler coordination and provides real-time metrics for monitoring - and decision-making across distributed worker processes. - - Example: - :: - from guidellm.scheduler.objects import SchedulerState - - # Initialize scheduler state - state = SchedulerState(node_id=0, num_processes=4) - - # Track request processing - state.created_requests += 1 - state.queued_requests += 1 - - # Monitor completion progress - completion_rate = state.processed_requests / state.created_requests - """ - - node_id: int = Field( - description="Unique identifier for this scheduler node", default=-1 - ) - num_processes: int = Field( - description="Number of worker processes in this scheduler", default=-1 - ) - start_time: float = Field( - description="Unix timestamp when the scheduler started", - default_factory=time.time, - ) - end_time: float | None = Field( - default=None, description="Unix timestamp when the scheduler stopped" - ) - end_queuing_time: float | None = Field( - default=None, description="When request queuing stopped, if applicable" - ) - end_queuing_constraints: dict[str, SchedulerUpdateAction] = Field( - default_factory=dict, - description="Constraints that triggered queuing termination", - ) - end_processing_time: float | None = Field( - default=None, description="When request processing stopped, if applicable" - ) - end_processing_constraints: dict[str, SchedulerUpdateAction] = Field( - default_factory=dict, - description="Constraints that triggered process ing termination", - ) - scheduler_constraints: dict[str, SchedulerUpdateAction] = Field( - default_factory=dict, - description=( - "The latest state from all constraints applied during the scheduler run" - ), - ) - - remaining_fraction: float | None = Field( - default=None, - description=( - "Estimated fraction for the remaining progress of the run, if known" - ), - ) - remaining_requests: float | None = Field( - default=None, - description="Estimated number of requests remaining to be processed, if known", - ) - remaining_duration: float | None = Field( - default=None, - description=( - "Estimated time remaining in seconds for the scheduler run, if known" - ), - ) - - created_requests: int = Field( - default=0, description="Total number of requests created" - ) - queued_requests: int = Field( - default=0, description="Total number of requests queued for processing" - ) - pending_requests: int = Field( - default=0, - description="Total number of requests pending processing within a worker", - ) - processing_requests: int = Field( - default=0, description="Number of requests currently being processed" - ) - processed_requests: int = Field( - default=0, description="Total number of requests that completed processing" - ) - successful_requests: int = Field( - default=0, description="Number of requests that completed successfully" - ) - errored_requests: int = Field( - default=0, description="Number of requests that failed with errors" - ) - cancelled_requests: int = Field( - default=0, description="Number of requests that were cancelled" - ) diff --git a/src/guidellm/scheduler/scheduler.py b/src/guidellm/scheduler/scheduler.py index e7d8b2c6..ca5935fa 100644 --- a/src/guidellm/scheduler/scheduler.py +++ b/src/guidellm/scheduler/scheduler.py @@ -1,11 +1,10 @@ """ -Thread-safe singleton scheduler for distributed load generation workload coordination. +Thread-safe singleton scheduler for distributed benchmarking workload coordination. -Provides the core orchestration engine that coordinates request processing across -worker processes and distributed environments. Manages timing synchronization, -resource allocation, constraint enforcement, and result aggregation for -load generation operations. Integrates with backends, environments, and strategies -to enable scalable load testing across various scenarios including LLM inference. +Orchestrates request processing across worker processes with distributed timing +coordination, constraint enforcement, and result aggregation. Integrates with +backends, environments, and strategies to enable scalable load testing across +various scenarios including LLM inference benchmarking. """ from __future__ import annotations @@ -13,21 +12,18 @@ from collections.abc import AsyncIterator, Iterable from typing import Any, Generic -from guidellm.scheduler.constraints import ( - Constraint, - ConstraintsInitializerFactory, -) +from guidellm.scheduler.constraints import Constraint, ConstraintsInitializerFactory from guidellm.scheduler.environments import Environment, NonDistributedEnvironment -from guidellm.scheduler.objects import ( +from guidellm.scheduler.schemas import ( BackendInterface, MultiTurnRequestT, RequestT, ResponseT, - ScheduledRequestInfo, SchedulerState, ) from guidellm.scheduler.strategies import SchedulingStrategy from guidellm.scheduler.worker_group import WorkerProcessGroup +from guidellm.schemas import RequestInfo from guidellm.utils.singleton import ThreadSafeSingletonMixin __all__ = ["Scheduler"] @@ -41,16 +37,14 @@ class Scheduler( Thread-safe singleton scheduler for distributed benchmarking workload coordination. Orchestrates request processing across worker processes with distributed timing - coordination, constraint enforcement, and result aggregation. Provides a unified - interface for executing benchmarking operations while abstracting the complexity - of multi-process coordination, environment synchronization, and resource management. - Implements singleton pattern to ensure consistent execution state across concurrent - benchmark operations. + coordination, constraint enforcement, and result aggregation. Abstracts the + complexity of multi-process coordination, environment synchronization, and + resource management while providing a unified interface for executing benchmarking + operations. Implements singleton pattern to ensure consistent execution state. Example: :: from guidellm.scheduler import Scheduler - from guidellm.backends import OpenAIBackend from guidellm.scheduler import NonDistributedEnvironment, SynchronousStrategy scheduler = Scheduler() @@ -61,7 +55,7 @@ class Scheduler( env=NonDistributedEnvironment(), max_requests=1000 ): - print(f"Processed: {request} with info: {info} and response: {response}") + print(f"Processed: {request}") """ async def run( @@ -69,13 +63,14 @@ async def run( requests: Iterable[RequestT | MultiTurnRequestT[RequestT]], backend: BackendInterface[RequestT, ResponseT], strategy: SchedulingStrategy, - env: Environment | None, - **constraints: dict[str, Any | dict[str, Any] | Constraint], + startup_duration: float, + env: Environment[RequestT, ResponseT] | None, + **constraints: Any | dict[str, Any] | Constraint, ) -> AsyncIterator[ tuple[ ResponseT | None, RequestT, - ScheduledRequestInfo, + RequestInfo, SchedulerState, ] ]: @@ -83,28 +78,29 @@ async def run( Execute distributed request processing with coordinated timing and constraints. Orchestrates the complete benchmarking workflow across worker processes with - environment synchronization, constraint enforcement, and error handling. - Manages resource lifecycle from initialization through cleanup while yielding - real-time processing updates for monitoring and aggregation. + environment synchronization, constraint enforcement, and error handling. Manages + resource lifecycle from initialization through cleanup while yielding real-time + processing updates for monitoring and aggregation. - :param requests: Request collection to process. Supports single requests or + :param requests: Request collection to process, supporting single requests or multi-turn sequences with optional inter-request delays :param backend: Backend interface for request processing and response generation :param strategy: Scheduling strategy controlling request timing and distribution + :param startup_duration: Duration in seconds for requests to ramp up :param env: Environment interface for distributed coordination and - synchronization + synchronization. Defaults to NonDistributedEnvironment if None :param constraints: Runtime constraints for execution control (max_requests, - max_duration, max_error_rate, etc.). Values can be primitives, dictionaries, - or constraint instances - :yields: Requests udpates as (response, request, request_info, scheduler_state) - tuples. Each request will generate three ordered updates: - queued, in_progress, completed | errored | cancelled. + max_duration, max_error_rate, etc.) as primitives, dictionaries, or + constraint instances + :yields: Request updates as (response, request, request_info, scheduler_state) + tuples. Each request generates three ordered updates: queued, in_progress, + completed | errored | cancelled :raises Exception: Worker process errors, environment synchronization failures, or constraint evaluation errors are propagated after cleanup """ with self.thread_lock: if env is None: - env = NonDistributedEnvironment() + env = NonDistributedEnvironment[RequestT, ResponseT]() worker_group: WorkerProcessGroup[RequestT, ResponseT] | None = None @@ -113,22 +109,22 @@ async def run( # and will ensure clean up before raising the error. try: # Setup local run parameters, sync with the environment - constraints = ConstraintsInitializerFactory.resolve_constraints( - constraints + resolved_constraints = ( + ConstraintsInitializerFactory.resolve_constraints(constraints) ) ( local_requests, local_strategy, local_constraints, - ) = await env.sync_run_params(requests, strategy, constraints) + ) = await env.sync_run_params(requests, strategy, resolved_constraints) # Setup the worker group, sync start with the environment worker_group = WorkerProcessGroup[RequestT, ResponseT]( - requests=None, - cycle_requests=local_requests, + requests=local_requests, backend=backend, strategy=local_strategy, - constraints=local_constraints, + startup_duration=startup_duration, + **local_constraints, ) await worker_group.create_processes() local_start_time = await env.sync_run_start() @@ -147,19 +143,20 @@ async def run( yield response, request, request_info, state except Exception as err: # noqa: BLE001 await env.sync_run_error(err) + raise err finally: # Ensure all worker processes are cleaned up for error or completion if worker_group is not None: - err = await worker_group.shutdown() + err = await worker_group.shutdown() # type: ignore[misc] if err is not None: await env.sync_run_error(err) # Ensure any errors are raised and all responses # are yielded for aggregation on the primary node async for ( - response, - request, - request_info, - state, + dist_response, + dist_request, + dist_request_info, + dist_state, ) in env.sync_run_end(): - yield response, request, request_info, state + yield dist_response, dist_request, dist_request_info, dist_state diff --git a/src/guidellm/scheduler/schemas.py b/src/guidellm/scheduler/schemas.py new file mode 100644 index 00000000..21567c67 --- /dev/null +++ b/src/guidellm/scheduler/schemas.py @@ -0,0 +1,272 @@ +""" +Core data structures and interfaces for the GuideLLM scheduler system. + +Provides type-safe abstractions for distributed request processing, timing +measurements, and backend interfaces for benchmarking operations. Central to +the scheduler architecture, enabling request lifecycle tracking, backend +coordination, and state management across distributed worker processes. +""" + +from __future__ import annotations + +import time +from collections.abc import AsyncIterator +from typing import Any, Generic, Literal, Protocol, TypeVar + +from pydantic import Field +from typing_extensions import TypeAliasType, TypedDict + +from guidellm.schemas import RequestInfo +from guidellm.utils import RegistryMixin, StandardBaseModel +from guidellm.utils.registry import RegistryObjT + +__all__ = [ + "BackendInterface", + "BackendT", + "MultiTurnRequestT", + "RequestT", + "ResponseT", + "SchedulerMessagingPydanticRegistry", + "SchedulerState", + "SchedulerUpdateAction", + "SchedulerUpdateActionProgress", +] + +RequestT = TypeVar("RequestT") +"Generic request object type for scheduler processing" + +ResponseT = TypeVar("ResponseT") +"Generic response object type returned by backend processing" + +MultiTurnRequestT = TypeAliasType( + "MultiTurnRequestT", + list[RequestT | tuple[RequestT, float]] | tuple[RequestT | tuple[RequestT, float]], + type_params=(RequestT,), +) +"Multi-turn request structure supporting conversation history with optional delays" + + +class SchedulerMessagingPydanticRegistry(RegistryMixin[RegistryObjT]): + """ + Registry for Pydantic types used in scheduler inter-process messaging. + + Enables generic interface for defining Pydantic class types used for + communication between distributed scheduler components and worker processes. + """ + + +class BackendInterface(Protocol, Generic[RequestT, ResponseT]): + """ + Protocol defining the interface for request processing backends. + + Establishes the contract for backend implementations that process requests + within the scheduler system. Backends manage initialization, validation, + processing, and shutdown lifecycle. All properties must be pickleable before + process_startup is called for multi-process environments. + + Example: + :: + class CustomBackend(BackendInterface): + @property + def processes_limit(self) -> int: + return 4 + + async def resolve(self, request, request_info, history=None): + yield response, updated_request_info + """ + + @property + def processes_limit(self) -> int | None: + """ + :return: Maximum worker processes supported, or None if unlimited + """ + + @property + def requests_limit(self) -> int | None: + """ + :return: Maximum concurrent requests supported, or None if unlimited + """ + + @property + def info(self) -> dict[str, Any]: + """ + :return: Backend metadata including model initialization and configuration + """ + + async def process_startup(self) -> None: + """ + Perform backend initialization and startup procedures. + + :raises Exception: Implementation-specific exceptions for startup failures + """ + + async def validate(self) -> None: + """ + Validate backend configuration and operational status. + + :raises Exception: Implementation-specific exceptions for validation failures + """ + + async def process_shutdown(self) -> None: + """ + Perform backend cleanup and shutdown procedures. + + :raises Exception: Implementation-specific exceptions for shutdown failures + """ + + async def resolve( + self, + request: RequestT, + request_info: RequestInfo, + history: list[tuple[RequestT, ResponseT]] | None = None, + ) -> AsyncIterator[tuple[ResponseT, RequestInfo]]: + """ + Process a request and yield incremental response updates. + + :param request: The request object to process + :param request_info: Scheduling metadata and timing information + :param history: Conversation history for multi-turn requests + :yield: Tuples of (response, updated_request_info) for each response chunk + :raises Exception: Implementation-specific exceptions for processing failures + """ + + +BackendT = TypeVar("BackendT", bound=BackendInterface) +"Generic backend interface type for request processing" + + +class SchedulerUpdateActionProgress(TypedDict, total=False): + """ + Progress tracking data for scheduler operations. + + Provides estimates for remaining work in scheduler operations, including + fraction complete, request counts, and duration. Used by constraints and + monitoring systems to track execution progress and make termination decisions. + """ + + remaining_fraction: float | None + remaining_requests: float | None + remaining_duration: float | None + + +class SchedulerUpdateAction(StandardBaseModel): + """ + Control directives for scheduler behavior and operations. + + Encapsulates control signals for scheduler operations including request + queuing and processing directives. Used by constraints to communicate + termination conditions and progress to scheduler components. + + Example: + :: + action = SchedulerUpdateAction( + request_queuing="stop", + request_processing="continue", + metadata={"reason": "max_requests_reached"} + ) + """ + + request_queuing: Literal["continue", "stop"] = Field( + default="continue", description="Action to take for request queuing operations" + ) + request_processing: Literal["continue", "stop_local", "stop_all"] = Field( + default="continue", + description="Action to take for request processing operations", + ) + metadata: dict[str, Any] = Field( + default_factory=dict, + description="Additional context and data for the scheduler action", + ) + progress: SchedulerUpdateActionProgress = Field( + default_factory=lambda: SchedulerUpdateActionProgress(), + description="Progress information for the scheduler action", + ) + + +class SchedulerState(StandardBaseModel): + """ + Comprehensive state tracking for scheduler execution. + + Tracks scheduler execution progress, request counts, timing information, + and constraint enforcement. Central to scheduler coordination, providing + real-time metrics for monitoring and decision-making across distributed + worker processes. + + Example: + :: + state = SchedulerState(node_id=0, num_processes=4) + state.created_requests += 1 + state.queued_requests += 1 + completion_rate = state.processed_requests / state.created_requests + """ + + node_id: int = Field( + description="Unique identifier for this scheduler node", default=-1 + ) + num_processes: int = Field( + description="Number of worker processes in this scheduler", default=-1 + ) + start_time: float = Field( + description="Unix timestamp when the scheduler started", + default_factory=time.time, + ) + end_time: float | None = Field( + default=None, description="Unix timestamp when the scheduler stopped" + ) + end_queuing_time: float | None = Field( + default=None, description="Unix timestamp when request queuing stopped" + ) + end_queuing_constraints: dict[str, SchedulerUpdateAction] = Field( + default_factory=dict, + description="Constraints that triggered queuing termination", + ) + end_processing_time: float | None = Field( + default=None, description="Unix timestamp when request processing stopped" + ) + end_processing_constraints: dict[str, SchedulerUpdateAction] = Field( + default_factory=dict, + description="Constraints that triggered processing termination", + ) + scheduler_constraints: dict[str, SchedulerUpdateAction] = Field( + default_factory=dict, + description="Latest state from all constraints applied during scheduler run", + ) + + remaining_fraction: float | None = Field( + default=None, + description="Estimated fraction of remaining progress, if known", + ) + remaining_requests: float | None = Field( + default=None, + description="Estimated number of remaining requests to process, if known", + ) + remaining_duration: float | None = Field( + default=None, + description="Estimated remaining time in seconds for scheduler run, if known", + ) + + created_requests: int = Field( + default=0, description="Total number of requests created" + ) + queued_requests: int = Field( + default=0, description="Total number of requests queued for processing" + ) + pending_requests: int = Field( + default=0, + description="Number of requests pending processing within a worker", + ) + processing_requests: int = Field( + default=0, description="Number of requests currently being processed" + ) + processed_requests: int = Field( + default=0, description="Number of requests that completed processing" + ) + successful_requests: int = Field( + default=0, description="Number of requests that completed successfully" + ) + errored_requests: int = Field( + default=0, description="Number of requests that failed with errors" + ) + cancelled_requests: int = Field( + default=0, description="Number of requests that were cancelled" + ) diff --git a/src/guidellm/scheduler/strategies.py b/src/guidellm/scheduler/strategies.py index 8c791671..0cd3bc63 100644 --- a/src/guidellm/scheduler/strategies.py +++ b/src/guidellm/scheduler/strategies.py @@ -1,34 +1,32 @@ """ -Request scheduling strategies for controlling how benchmark requests are processed. +Request scheduling strategies for controlling benchmark request processing patterns. -This module provides timing implementations and concrete strategies that control request +Provides timing implementations and concrete strategies that control request concurrency, timing patterns, and throughput characteristics to simulate real-world -usage scenarios. The scheduling system separates timing logic from strategy constraints, -enabling flexible combination of timing behaviors with process and concurrency limits. +usage scenarios. Strategies define how requests are distributed across worker processes, +when they should be scheduled, and what constraints apply to concurrent processing. +The scheduling system separates timing logic from strategy constraints, enabling +flexible combination of timing behaviors with process and concurrency limits. """ from __future__ import annotations -import math +import asyncio import random import time -from abc import ABC, abstractmethod +from abc import abstractmethod +from multiprocessing import Lock, Value from typing import Annotated, ClassVar, Literal, TypeVar from pydantic import Field, PrivateAttr -from guidellm.scheduler.objects import ScheduledRequestInfo -from guidellm.utils import InfoMixin, PydanticClassRegistryMixin, StandardBaseModel +from guidellm.schemas import RequestInfo +from guidellm.utils import InfoMixin, PydanticClassRegistryMixin __all__ = [ "AsyncConstantStrategy", "AsyncPoissonStrategy", "ConcurrentStrategy", - "ConstantRateRequestTimings", - "LastCompletionRequestTimings", - "NoDelayRequestTimings", - "PoissonRateRequestTimings", - "ScheduledRequestTimings", "SchedulingStrategy", "StrategyT", "StrategyType", @@ -43,308 +41,162 @@ ] -def _exponential_decay_tau(max_progress: float, convergence: float = 0.99) -> float: - """ - Calculate tau value for exponential decay to reach target progress level. - - :param max_progress: The max progress value to reach - :param convergence: The target convergence level for reaching max_progress - :return: The calculated tau value for the given max_progress and convergence - """ - return max_progress / (-math.log(1 - convergence)) - - -def _exponential_decay_fraction(progress: float, tau: float = 1.0) -> float: - """ - Calculate completion fraction based on exponential decay curve. - - :param progress: The current progress value (>=0) - :param tau: The scale factor for the exponential decay - :return: The fraction of completion based on exponential decay (0 -> 1) +class SchedulingStrategy(PydanticClassRegistryMixin["SchedulingStrategy"], InfoMixin): """ - return 1 - math.exp(-progress / tau) + Base class for scheduling strategies controlling request processing patterns. + Defines the interface for strategies that combine timing implementations with + process and concurrency constraints to enable various benchmark scenarios. + Strategies manage request timing, worker process coordination, and concurrency + limits across distributed execution environments. -class ScheduledRequestTimings(StandardBaseModel, ABC): - """ - Abstract base class for controlling when requests are scheduled. - - Defines the interface for timing implementations that determine request scheduling - behavior. Different implementations provide various patterns like synchronous, - constant-rate, or stochastic scheduling to simulate real-world scenarios. + :cvar schema_discriminator: Field name used for polymorphic deserialization """ - @abstractmethod - def next_offset(self) -> float: - """ - Calculate the time offset for the next request to be scheduled. - - :return: The offset in seconds from scheduler start time for next request - """ - - @abstractmethod - def request_completed(self, request_info: ScheduledRequestInfo): - """ - Handle request completion and update internal timing state. - - :param request_info: Information about the completed request including - timing details and completion status - """ - + schema_discriminator: ClassVar[str] = "type_" -class LastCompletionRequestTimings(ScheduledRequestTimings): - """ - Timing implementation for synchronous and concurrent scheduling strategies. + @classmethod + def __pydantic_schema_base_type__(cls) -> type[SchedulingStrategy]: + if cls.__name__ == "SchedulingStrategy": + return cls - Schedules the next request immediately after the last request completes, enabling - sequential or limited concurrent processing with completion-based timing control. - """ + return SchedulingStrategy - offset: float = Field( - default=0.0, - description="Current time offset in seconds from scheduler start time", + type_: Literal["strategy"] = Field( + description="The type of scheduling strategy to schedule requests with", ) - startup_requests: int = Field( + worker_count: int = Field( default=0, - description="Number of initial requests to schedule with equal spacing", + description="Number of worker processes to use for this strategy", ge=0, ) - startup_requests_delay: float = Field( - default=0.0, - description="Delay in seconds between startup requests", - ge=0, - ) - _requests_count: int = PrivateAttr(0) - - def next_offset(self) -> float: - """ - Get the current offset value and apply startup delay if applicable. - - :return: The current offset value in seconds from scheduler start time - """ - self._requests_count += 1 - - if self._requests_count <= self.startup_requests: - self.offset += self.startup_requests_delay - - return self.offset - - def request_completed(self, request_info: ScheduledRequestInfo): - """ - Update timing state based on the completed request. - - :param request_info: Information about the completed request - """ - if ( - self._requests_count > self.startup_requests - and request_info.completed_at is not None - ): - # set the next sync offset to the time when the previous request completed - self.offset = request_info.completed_at - request_info.scheduler_start_time - - -class NoDelayRequestTimings(ScheduledRequestTimings): - """ - Timing implementation for throughput-maximizing scheduling strategies. - - Schedules requests with minimal delay to achieve maximum throughput, with optional - startup ramping to gradually increase request processing during initialization. - """ - - offset: float = Field( - default=0.0, - description="Base time offset in seconds from scheduler start time", + max_concurrency: int = Field( + default=0, + description="Maximum number of concurrent requests to allow", ge=0, ) startup_duration: float = Field( default=0.0, - description="Duration in seconds for gradual startup ramp", + description="Duration in seconds for startup request distribution", ge=0, ) - startup_target_requests: int = Field( - default=1, - description="Target number of requests to converge to during startup", - gt=0, - ) - startup_convergence: float = Field( - default=0.99, - description="Target convergence rate during startup phase", - ) - _start_time: float | None = PrivateAttr(None) - _requests_count: int = PrivateAttr(0) - - def next_offset(self) -> float: - """ - Calculate offset with optional startup adjustment. - - :return: Static offset plus any startup adjustment - """ - if self._start_time is None: - self._start_time = time.time() - self._requests_count += 1 - elapsed = time.time() - self._start_time + _processes_lock = PrivateAttr(None) + _processes_request_index = PrivateAttr(None) + _processes_start_time = PrivateAttr(None) + _cached_processes_start_time: float | None = PrivateAttr(None) - if self.startup_duration > 0 and elapsed < self.startup_duration: - startup_percent = _exponential_decay_fraction( - self._requests_count, - _exponential_decay_tau( - self.startup_target_requests, self.startup_convergence - ), - ) - else: - startup_percent = 1.0 - - return self.offset + startup_percent * self.startup_duration - - def request_completed(self, request_info: ScheduledRequestInfo): + @property + def processes_limit(self) -> int | None: """ - Handle request completion (no action needed for throughput strategy). + Get the maximum number of worker processes supported by this strategy. - :param request_info: Information about the completed request (unused) + :return: Maximum number of worker processes, None if unlimited """ + return None - -class ConstantRateRequestTimings(ScheduledRequestTimings): - """ - Timing implementation for constant-rate scheduling strategies. - - Schedules requests at a fixed rate with evenly spaced intervals to provide - predictable timing behavior for steady-state load simulation. - """ - - rate: float = Field( - description="Target rate in requests per second", - gt=0, - ) - offset: float = Field( - default=0.0, - description="Base time offset in seconds from scheduler start time", - ge=0, - ) - _requests_count: int = PrivateAttr(0) - - def next_offset(self) -> float: + @property + def requests_limit(self) -> int | None: """ - Calculate the offset for the next request at a constant rate. + Get the maximum number of concurrent requests supported by this strategy. - :return: The offset in seconds for the next request + :return: Maximum number of concurrent requests, None if unlimited """ - num_requests = self._requests_count - self._requests_count += 1 - interval = 1.0 / self.rate - - return self.offset + interval * num_requests + return None - def request_completed(self, request_info: ScheduledRequestInfo): + def init_processes_timings( + self, + worker_count: int, + max_concurrency: int, + startup_duration: float, + ): """ - Handle request completion (no action needed for constant rate strategy). + Initialize shared timing state for multi-process coordination. - :param request_info: Information about the completed request (unused) + :param worker_count: Number of worker processes to coordinate + :param max_concurrency: Maximum number of concurrent requests allowed + :param startup_duration: Duration in seconds for request startup ramping """ + self.worker_count = worker_count + self.max_concurrency = max_concurrency + self.startup_duration = startup_duration + self._processes_request_index = Value("i", 0) + self._processes_lock = Lock() + self._processes_start_time = Value("d", -1.0) -class PoissonRateRequestTimings(ScheduledRequestTimings): - """ - Timing implementation for Poisson-distributed scheduling strategies. - - Schedules requests following a Poisson process with exponentially distributed - inter-arrival times to simulate realistic traffic patterns with random variance. - """ - - rate: float = Field( - description="Target average rate in requests per second", - gt=0, - ) - random_seed: int = Field( - default=42, - description="Seed for random number generator for reproducible behavior", - ) - offset: float = Field( - default=0.0, - description="Base time offset in seconds from scheduler start time", - ) - _requests_count: int = PrivateAttr(0) - _random: random.Random | None = PrivateAttr(None) - - def next_offset(self) -> float: + def init_processes_start(self, start_time: float): """ - Calculate the offset for the next request using Poisson distribution. + Set the synchronized start time for all worker processes. - :return: The cumulative offset in seconds for the next request + :param start_time: Unix timestamp when request processing should begin + :raises RuntimeError: If called before init_processes_timings """ - self._requests_count += 1 - - if self._random is None: - self._random = random.Random(self.random_seed) - else: - next_delay = self._random.expovariate(self.rate) - self.offset += next_delay + if self._processes_lock is None: + raise RuntimeError( + "SchedulingStrategy init_processes_start called before " + "init_processes_timings" + ) - return self.offset + with self._processes_lock: + self._processes_start_time.value = start_time - def request_completed(self, request_info: ScheduledRequestInfo): + async def get_processes_start_time(self) -> float: """ - Handle request completion (no action needed for Poisson rate strategy). + Get the synchronized start time, waiting if not yet set. - :param request_info: Information about the completed request (unused) + :return: Unix timestamp when request processing began + :raises RuntimeError: If called before init_processes_timings """ + if self._processes_lock is None: + raise RuntimeError( + "SchedulingStrategy get_processes_start_time called before " + "init_processes_timings" + ) + while self._cached_processes_start_time is None: + with self._processes_lock: + if self._processes_start_time.value != -1.0: + self._cached_processes_start_time = self._processes_start_time.value + else: + await asyncio.sleep(0.01) # wait for start time to be set by main -class SchedulingStrategy(PydanticClassRegistryMixin["SchedulingStrategy"], InfoMixin): - """ - Abstract base class for scheduling strategies controlling request processing. - - Defines the interface for strategies that combine timing implementations with - process and concurrency constraints to enable various benchmark scenarios. - """ - - schema_discriminator: ClassVar[str] = "type_" - - @classmethod - def __pydantic_schema_base_type__(cls) -> type[SchedulingStrategy]: - if cls.__name__ == "SchedulingStrategy": - return cls - - return SchedulingStrategy - - type_: Literal["strategy"] = Field( - description="The type of scheduling strategy to schedule requests with", - ) + return self._cached_processes_start_time - @property - def processes_limit(self) -> int | None: + def next_request_index(self) -> int: """ - Get the maximum number of worker processes supported by this strategy. + Get the next sequential request index across all worker processes. - :return: Maximum number of worker processes, None if unlimited + :return: Globally unique request index for timing calculations + :raises RuntimeError: If called before init_processes_timings """ - return None + if self._processes_lock is None: + raise RuntimeError( + "SchedulingStrategy next_request_index called before " + "init_processes_timings" + ) - @property - def requests_limit(self) -> int | None: + with self._processes_lock: + self._processes_request_index.value += 1 + return self._processes_request_index.value + + @abstractmethod + async def next_request_time(self, offset: int) -> float: """ - Get the maximum number of concurrent requests supported by this strategy. + Calculate the scheduled start time for the next request. - :return: Maximum number of concurrent requests, None if unlimited + :param offset: Worker process offset for distributing request timing + :return: Unix timestamp when the request should be processed """ - return None - def create_request_timings( - self, local_rank: int, local_world_size: int, local_max_concurrency: int - ) -> ScheduledRequestTimings: + @abstractmethod + def request_completed(self, request_info: RequestInfo): """ - Create a timing instance to define scheduling behavior for a worker process. + Handle request completion and update internal timing state. - :param local_rank: The rank of the worker process within local world size - :param local_world_size: Total number of worker processes in local world - :param local_max_concurrency: Maximum concurrent requests for the worker - :return: A ScheduledRequestTimings instance for the worker process - :raises NotImplementedError: Must be implemented by subclasses + :param request_info: Information about the completed request including + timing details and completion status """ - raise NotImplementedError( - "create_worker_timings method must be implemented by subclasses." - ) StrategyT = TypeVar("StrategyT", bound=SchedulingStrategy) @@ -353,19 +205,18 @@ def create_request_timings( @SchedulingStrategy.register("synchronous") class SynchronousStrategy(SchedulingStrategy): """ - Sequential request processing strategy with single-process constraint. + Sequential request processing with strict single-request-at-a-time execution. Processes requests one at a time in strict sequential order, providing predictable timing behavior ideal for measuring maximum sequential throughput and ensuring - request isolation. + complete request isolation. Each request completes before the next begins. """ type_: Literal["synchronous"] = "synchronous" # type: ignore[assignment] + _process_last_request_time: float | None = PrivateAttr(None) def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier for synchronous strategy """ return "synchronous" @@ -373,52 +224,49 @@ def __str__(self) -> str: @property def processes_limit(self) -> int | None: """ - Get maximum number of worker processes for synchronous scheduling. - - :return: Always returns 1 to enforce single-process constraint + :return: Always 1 to enforce single-process constraint """ return 1 @property def requests_limit(self) -> int | None: """ - Get maximum number of concurrent requests for synchronous scheduling. - - :return: Always returns 1 to enforce single-request constraint + :return: Always 1 to enforce single-request constraint """ return 1 - def create_request_timings( - self, - local_rank: int, - local_world_size: int, - local_max_concurrency: int, # noqa: ARG002 - ) -> ScheduledRequestTimings: - """ - Create timing implementation for synchronous request scheduling. - - :param local_rank: The rank of the worker process (must be 0) - :param local_world_size: Total number of worker processes (must be 1) - :param local_max_concurrency: Maximum concurrent requests (unused) - :return: LastCompletionRequestTimings instance for sequential processing - :raises ValueError: If multiple workers or non-zero rank specified - """ - if local_world_size > 1 or local_rank != 0: - raise ValueError( - "SynchronousStrategy can only be used with a single worker process." - ) + async def next_request_time(self, offset: int) -> float: + """ + Calculate next request time based on previous completion. - return LastCompletionRequestTimings() + :param offset: Unused for synchronous strategy + :return: Time of last completion or start time if first request + """ + _ = offset # offset unused for synchronous strategy + + if self._process_last_request_time is not None: + return self._process_last_request_time + + return await self.get_processes_start_time() + + def request_completed(self, request_info: RequestInfo): + """ + Update timing state with completed request information. + + :param request_info: Completed request metadata including timing + """ + if request_info.completed_at is not None: + self._process_last_request_time = request_info.completed_at @SchedulingStrategy.register("concurrent") class ConcurrentStrategy(SchedulingStrategy): """ - Parallel request processing strategy with controlled concurrency limits. + Parallel request processing with fixed concurrency limits. Enables concurrent request processing up to a specified number of streams, - providing balanced throughput while maintaining predictable resource usage - and completion-based timing coordination. + providing balanced throughput while maintaining predictable resource usage. + Requests are distributed across streams with completion-based timing coordination. """ type_: Literal["concurrent"] = "concurrent" # type: ignore[assignment] @@ -426,16 +274,11 @@ class ConcurrentStrategy(SchedulingStrategy): description="Number of concurrent streams for scheduling requests", gt=0, ) - startup_duration: float = Field( - default=0.0, - description="Duration in seconds for distributing startup requests", - ge=0, - ) + + _process_last_request_time: float | None = PrivateAttr(None) def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier with stream count """ return f"concurrent@{self.streams}" @@ -443,8 +286,6 @@ def __str__(self) -> str: @property def processes_limit(self) -> int: """ - Get maximum number of worker processes for concurrent scheduling. - :return: Number of streams as maximum worker processes """ return self.streams @@ -452,72 +293,42 @@ def processes_limit(self) -> int: @property def requests_limit(self) -> int: """ - Get maximum number of concurrent requests for concurrent scheduling. - :return: Number of streams as maximum concurrent requests """ return self.streams - def create_request_timings( - self, - local_rank: int, - local_world_size: int, - local_max_concurrency: int, # noqa: ARG002 - ) -> LastCompletionRequestTimings: - """ - Create timing implementation for concurrent request scheduling. - - :param local_rank: The rank of the worker process (must be < streams) - :param local_world_size: Total worker processes (must not exceed streams) - :param local_max_concurrency: Maximum concurrent requests (unused) - :return: LastCompletionRequestTimings instance for stream-based processing - :raises ValueError: If worker configuration exceeds stream limits - """ - if local_world_size > self.streams: - raise ValueError( - "ConcurrentStrategy can only be used with up to " - f"{self.streams} worker processes." - ) + async def next_request_time(self, offset: int) -> float: + """ + Calculate next request time with stream-based distribution. - if local_rank >= self.streams: - raise ValueError( - f"Local rank {local_rank} exceeds the number of streams {self.streams}." - ) + :param offset: Worker process offset for distributing initial requests + :return: Time of last completion or staggered start time if first request + """ + if self._process_last_request_time is not None: + return self._process_last_request_time - if self.startup_duration > 0: - # Ensure equal global distribution of the start up for concurrent streams - # Ex: for 10 streams, 2 workers, and 8 seconds start up duration, - # the first worker should start at 0.0, 1.6, 3.2, 4.8, 6.4 - # and the second worker should start at 0.8, 2.4, 4.0, 5.6, 7.2 - delay_per_stream = self.startup_duration / self.streams - streams_per_worker = self.streams // local_world_size - - offset = local_rank * streams_per_worker * delay_per_stream - startup_requests = streams_per_worker + ( - 1 - if local_world_size > 1 and local_rank < self.streams % local_world_size - else 0 - ) - startup_requests_delay = delay_per_stream * local_world_size - else: - offset = 0.0 - startup_requests = 0 - startup_requests_delay = 0.0 + start_time = await self.get_processes_start_time() + + return start_time + (offset / self.worker_count) - return LastCompletionRequestTimings( - offset=offset, - startup_requests=startup_requests, - startup_requests_delay=startup_requests_delay, - ) + def request_completed(self, request_info: RequestInfo): + """ + Update timing state with completed request information. + + :param request_info: Completed request metadata including timing + """ + if request_info.completed_at is not None: + self._process_last_request_time = request_info.completed_at @SchedulingStrategy.register("throughput") class ThroughputStrategy(SchedulingStrategy): """ - Maximum throughput strategy with optional concurrency limits. + Maximum throughput scheduling with optional concurrency limits. Schedules requests to maximize system throughput by allowing unlimited concurrent - processing with optional constraints and startup ramping for controlled ramp-up. + processing with optional constraints. Supports startup ramping to gradually + distribute initial requests for controlled system ramp-up. """ type_: Literal["throughput"] = "throughput" # type: ignore[assignment] @@ -526,16 +337,9 @@ class ThroughputStrategy(SchedulingStrategy): description="Maximum number of concurrent requests to schedule", gt=0, ) - startup_duration: float = Field( - default=0.0, - description="Duration in seconds for startup request distribution", - ge=0, - ) def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier for throughput strategy """ return "throughput" @@ -543,56 +347,57 @@ def __str__(self) -> str: @property def processes_limit(self) -> int | None: """ - Get maximum number of worker processes for throughput scheduling. - - :return: The max_concurrency value if set, otherwise None for unlimited + :return: Max concurrency if set, otherwise None for unlimited """ return self.max_concurrency @property def requests_limit(self) -> int | None: """ - Get maximum number of concurrent requests for throughput scheduling. - - :return: The max_concurrency value if set, otherwise None for unlimited + :return: Max concurrency if set, otherwise None for unlimited """ return self.max_concurrency - def create_request_timings( - self, local_rank: int, local_world_size: int, local_max_concurrency: int - ) -> ScheduledRequestTimings: + async def next_request_time(self, offset: int) -> float: """ - Create timing implementation for throughput request scheduling. + Calculate next request time with optional startup ramping. + + :param offset: Unused for throughput strategy + :return: Immediate start or ramped start time during startup period + """ + _ = offset # offset unused for throughput strategy + start_time = await self.get_processes_start_time() + + if ( + self.startup_duration > 0 + and (time.time() - start_time) < self.startup_duration + and (current_index := self.next_request_index()) <= self.max_concurrency + ): + # linearly ramp start times to spread max_concurrency requests evenly + # over startup_duration + return start_time + self.startup_duration * ( + current_index / self.max_concurrency + ) - :param local_rank: The rank of the worker process - :param local_world_size: Total number of worker processes - :param local_max_concurrency: Maximum concurrent requests for the worker - :return: NoDelayRequestTimings instance for immediate request scheduling + return start_time + self.startup_duration + + def request_completed(self, request_info: RequestInfo): """ - if self.startup_duration > 0: - # Vary offset by up to 5% of the startup duration for a bit of variance - offset = 0.05 * self.startup_duration * (local_rank / local_world_size) - # Use local_max_concurrency as the target requests for startup convergence - startup_target_requests = local_max_concurrency - else: - offset = 0.0 - startup_target_requests = 1 + Handle request completion (no-op for throughput strategy). - return NoDelayRequestTimings( - startup_duration=self.startup_duration, - startup_target_requests=startup_target_requests, - offset=offset, - ) + :param request_info: Completed request metadata (unused) + """ + _ = request_info # request_info unused for throughput strategy @SchedulingStrategy.register("constant") class AsyncConstantStrategy(ThroughputStrategy): """ - Asynchronous constant-rate scheduling strategy for predictable load patterns. + Constant-rate scheduling for predictable load patterns. Schedules requests at a fixed rate distributed evenly across worker processes, providing predictable timing behavior for steady-state load simulation and - consistent system performance measurement. + consistent system performance measurement. Requests arrive at uniform intervals. """ type_: Literal["constant"] = "constant" # type: ignore[assignment] @@ -600,53 +405,43 @@ class AsyncConstantStrategy(ThroughputStrategy): description="Rate for scheduling requests asynchronously in requests/second", gt=0, ) - startup_duration: float = Field( - default=0.0, - description="Duration in seconds for startup request distribution", - ge=0, - ) def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier with rate value """ return f"constant@{self.rate:.2f}" - def create_request_timings( - self, - local_rank: int, - local_world_size: int, - local_max_concurrency: int, # noqa: ARG002 - ) -> ScheduledRequestTimings: + async def next_request_time(self, offset: int) -> float: """ - Create timing implementation for constant-rate request scheduling. + Calculate next request time at fixed intervals. - :param local_rank: The rank of the worker process - :param local_world_size: Total number of worker processes for rate division - :param local_max_concurrency: Maximum concurrent requests for the worker - :return: ConstantRateRequestTimings instance with per-worker rate + :param offset: Unused for constant strategy + :return: Start time plus constant interval based on request index """ - # Divide the rate evenly across all worker processes - worker_rate = self.rate / local_world_size - # Start each worker with an offset to interleave rates - worker_offset = (1 / self.rate) * local_rank + _ = offset # offset unused for throughput strategy + current_index = self.next_request_index() + start_time = await self.get_processes_start_time() + + return start_time + current_index / self.rate - return ConstantRateRequestTimings( - rate=worker_rate, - offset=worker_offset, - ) + def request_completed(self, request_info: RequestInfo): + """ + Handle request completion (no-op for constant strategy). + + :param request_info: Completed request metadata (unused) + """ + _ = request_info # request_info unused for async constant strategy @SchedulingStrategy.register("poisson") class AsyncPoissonStrategy(ThroughputStrategy): """ - Asynchronous Poisson-distributed scheduling strategy for realistic load simulation. + Poisson-distributed scheduling for realistic load simulation. Schedules requests following a Poisson process with exponentially distributed inter-arrival times, providing realistic simulation of user behavior and network - traffic patterns with random variance around the target rate. + traffic patterns. Request arrivals have random variance around the target rate. """ type_: Literal["poisson"] = "poisson" # type: ignore[assignment] @@ -654,47 +449,71 @@ class AsyncPoissonStrategy(ThroughputStrategy): description="Rate for scheduling requests asynchronously in requests/second", gt=0, ) - startup_duration: float = Field( - default=0.0, - description="Duration in seconds for startup request distribution", - ge=0, - ) random_seed: int = Field( default=42, description="Random seed to use for Poisson distribution", ) + _random: random.Random | None = PrivateAttr(None) + _offset = PrivateAttr(None) + def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier with rate value """ return f"poisson@{self.rate:.2f}" - def create_request_timings( + def init_processes_timings( self, - local_rank: int, - local_world_size: int, - local_max_concurrency: int, # noqa: ARG002 - ) -> ScheduledRequestTimings: - """ - Create timing implementation for Poisson-distributed request scheduling. - - :param local_rank: The rank of the worker process for seed generation - :param local_world_size: Total number of worker processes for rate division - :param local_max_concurrency: Maximum concurrent requests for the worker - :return: PoissonRateRequestTimings instance with per-worker rate and unique seed - """ - # Divide the rate evenly across all worker processes - worker_rate = self.rate / local_world_size - # Use a different seed for each worker to ensure different sequences - worker_seed = self.random_seed + local_rank - # Start each worker with an offset to interleave rates - worker_offset = (1 / self.rate) * local_rank - - return PoissonRateRequestTimings( - rate=worker_rate, - random_seed=worker_seed, - offset=worker_offset, - ) + worker_count: int, + max_concurrency: int, + startup_duration: float, + ): + """ + Initialize Poisson-specific timing state. + + :param worker_count: Number of worker processes to coordinate + :param max_concurrency: Maximum number of concurrent requests allowed + :param startup_duration: Duration in seconds for request startup ramping + """ + super().init_processes_timings(worker_count, max_concurrency, startup_duration) + with self._processes_lock: + self._offset = Value("d", -1.0) + + def init_processes_start(self, start_time: float): + """ + Initialize the offset time for Poisson timing calculations. + + :param start_time: Unix timestamp when request processing should begin + """ + ThroughputStrategy.init_processes_start(self, start_time) + with self._processes_lock: + self._offset.value = start_time + + async def next_request_time(self, offset: int) -> float: + """ + Calculate next request time using exponential distribution. + + :param offset: Unused for Poisson strategy + :return: Next arrival time based on Poisson process + """ + _ = offset # offset unused for throughput strategy + _ = await self.get_processes_start_time() # ensure offset is initialized + + if self._random is None: + self._random = random.Random(self.random_seed) + + next_delay = self._random.expovariate(self.rate) + + with self._processes_lock: + self._offset.value += next_delay + + return self._offset.value + + def request_completed(self, request_info: RequestInfo): + """ + Handle request completion (no-op for Poisson strategy). + + :param request_info: Completed request metadata (unused) + """ + _ = request_info # request_info unused for async poisson strategy diff --git a/src/guidellm/scheduler/worker.py b/src/guidellm/scheduler/worker.py index 5f2fb74b..a46455f9 100644 --- a/src/guidellm/scheduler/worker.py +++ b/src/guidellm/scheduler/worker.py @@ -1,10 +1,11 @@ """ -Individual worker process management for multi-process request execution. +Worker process implementation for distributed request execution and coordination. -Manages worker processes that handle request scheduling, backend processing, and -coordination in distributed benchmark environments. Workers consume requests from -queues, apply timing strategies, process requests through backends, and publish -status updates while maintaining synchronization across the process group. +Manages individual worker processes within the scheduler system, handling request +lifecycle from queue consumption through backend processing and status publication. +Workers coordinate with other processes through barriers and events, apply timing +strategies for request scheduling, maintain concurrency limits, and publish real-time +status updates throughout request processing. """ from __future__ import annotations @@ -19,25 +20,24 @@ import uvloop HAS_UVLOOP: Annotated[ - bool, "Flag indicating if uvloop is available for event loop optimization" + bool, "Flag indicating uvloop availability for event loop optimization" ] = True except ImportError: uvloop = None HAS_UVLOOP: Annotated[ - bool, "Flag indicating if uvloop is available for event loop optimization" + bool, "Flag indicating uvloop availability for event loop optimization" ] = False -from guidellm.scheduler.objects import ( +from guidellm.scheduler.schemas import ( BackendInterface, MultiTurnRequestT, RequestT, ResponseT, - ScheduledRequestInfo, - SchedulerMessagingPydanticRegistry, ) -from guidellm.scheduler.strategies import ScheduledRequestTimings +from guidellm.scheduler.strategies import SchedulingStrategy +from guidellm.schemas import RequestInfo from guidellm.utils import ( InterProcessMessaging, wait_for_sync_barrier, @@ -50,39 +50,45 @@ class WorkerProcess(Generic[RequestT, ResponseT]): """ - Individual worker process for distributed request execution and coordination. + Worker process for distributed request execution in the scheduler system. - Manages the complete request lifecycle from queue consumption through backend - processing and status publication. Coordinates with other workers through - barriers and events while maintaining configurable concurrency limits and - timing strategies for request scheduling. + Manages complete request lifecycle including queue consumption, backend processing, + timing strategy application, and status publication. Coordinates with other workers + through synchronization primitives while maintaining concurrency limits and handling + graceful shutdown scenarios including errors and cancellations. Example: :: worker = WorkerProcess( + worker_index=0, messaging=messaging_interface, + backend=backend_instance, + strategy=timing_strategy, async_limit=10, + fut_scheduling_time_limit=5.0, startup_barrier=barrier, + requests_generated_event=generated_event, + constraint_reached_event=constraint_event, shutdown_event=shutdown, error_event=error, - backend=backend_instance, - request_timings=timing_strategy ) worker.run() """ def __init__( self, + worker_index: int, messaging: InterProcessMessaging[ tuple[ ResponseT | None, RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, + RequestInfo, ], ], backend: BackendInterface[RequestT, ResponseT], - request_timings: ScheduledRequestTimings, + strategy: SchedulingStrategy, async_limit: int, + fut_scheduling_time_limit: float, startup_barrier: ProcessingBarrier, requests_generated_event: ProcessingEvent, constraint_reached_event: ProcessingEvent, @@ -92,22 +98,25 @@ def __init__( """ Initialize worker process instance. - :param messaging: Inter-process communication interface for request coordination - :param backend: Backend instance for processing requests - :param request_timings: Timing strategy for request scheduling - :param async_limit: Maximum concurrent requests this worker can handle - :param startup_barrier: Multiprocessing barrier for coordinated startup - :param requests_generated_event: Event signaling when request generation is - complete - :param constraint_reached_event: Event signaling when processing constraints - are met - :param shutdown_event: Event for signaling graceful shutdown - :param error_event: Event for signaling error conditions across processes + :param worker_index: Unique identifier for this worker within the process group + :param messaging: Inter-process messaging interface for request coordination + :param backend: Backend interface for processing requests + :param strategy: Scheduling strategy for determining request timing + :param async_limit: Maximum concurrent requests this worker can process + :param fut_scheduling_time_limit: Maximum time in seconds to schedule requests + into the future + :param startup_barrier: Synchronization barrier for coordinated startup + :param requests_generated_event: Event signaling request generation completion + :param constraint_reached_event: Event signaling processing constraint reached + :param shutdown_event: Event signaling graceful shutdown request + :param error_event: Event signaling error conditions across processes """ + self.worker_index = worker_index self.messaging = messaging self.backend = backend - self.request_timings = request_timings + self.strategy = strategy self.async_limit = async_limit + self.fut_scheduling_time_limit = fut_scheduling_time_limit self.startup_barrier = startup_barrier self.requests_generated_event = requests_generated_event self.constraint_reached_event = constraint_reached_event @@ -123,8 +132,8 @@ def run(self): """ Main entry point for worker process execution. - Initializes asyncio event loop with optional uvloop optimization and starts - worker async operations. Handles event loop cleanup for forked processes. + Initializes asyncio event loop with optional uvloop optimization and executes + worker async operations. Handles event loop cleanup and error propagation. :raises RuntimeError: If worker encounters unrecoverable error during execution """ @@ -143,9 +152,9 @@ async def run_async(self): """ Execute main asynchronous worker process logic. - Orchestrates concurrent execution of request processing and shutdown monitoring - tasks. Handles task cleanup, error propagation, and cancellation coordination - when any task completes or fails. + Orchestrates concurrent execution of request processing and shutdown monitoring. + Handles task cleanup, error propagation, and cancellation coordination when any + task completes or encounters an error. :raises RuntimeError: If worker tasks encounter unrecoverable errors :raises asyncio.CancelledError: If worker process was cancelled @@ -193,6 +202,7 @@ async def run_async(self): async def _stop_monitor( self, ) -> Literal["error_event", "shutdown_event"]: + """Monitor shutdown and error events for worker termination.""" exit_key = await wait_for_sync_objects( { "error_event": self.error_event, @@ -207,6 +217,12 @@ async def _stop_monitor( ) async def _process_requests(self): + """ + Manage request processing lifecycle from startup to shutdown. + + Coordinates startup synchronization, processes requests until constraints are + reached, then cancels pending requests until shutdown or error occurs. + """ try: # 1. Start up synchronization (backend, messaging, and other processes) # 2. Messaging startup, receive requests until requests_generated event @@ -228,6 +244,7 @@ async def _process_requests(self): await self._processing_shutdown() async def _processing_startup(self): + """Initialize backend, messaging, and synchronize with other workers.""" # Get backend ready await self.backend.process_startup() self.backend_started = True @@ -235,8 +252,7 @@ async def _processing_startup(self): # Get messaging system ready await self.messaging.start( - receive_stop_criteria=[self.requests_generated_event], - pydantic_models=list(SchedulerMessagingPydanticRegistry.registry.values()), + receive_stop_criteria=[self.requests_generated_event] ) self.messaging_started = True @@ -260,6 +276,12 @@ async def _processing_shutdown(self): self.startup_completed = False async def _process_requests_loop(self): + """ + Process requests continuously until cancelled with concurrency limits. + + Schedules and processes requests according to the timing strategy while + maintaining the configured concurrency limit through semaphore coordination. + """ try: # Run request processing async_semaphore = asyncio.Semaphore(self.async_limit) @@ -275,7 +297,18 @@ def _task_done(task): # Main loop; loop until canceled while True: await async_semaphore.acquire() - request_task = asyncio.create_task(self._process_next_request()) + request_time = await self.strategy.next_request_time( + offset=self.worker_index + ) + + if ( + time_until := request_time - time.time() + ) >= self.fut_scheduling_time_limit: + await asyncio.sleep(time_until - self.fut_scheduling_time_limit) + + request_task = asyncio.create_task( + self._process_next_request(target_start=request_time) + ) pending_tasks.add(request_task) request_task.add_done_callback(_task_done) except asyncio.CancelledError as err: @@ -286,59 +319,65 @@ def _task_done(task): raise err async def _cancel_requests_loop(self): + """Cancel all remaining queued requests until worker process terminates.""" while True: try: request: RequestT - request_info: ScheduledRequestInfo + request_info: RequestInfo request, request_info = await self.messaging.get( timeout=self.messaging.poll_interval ) except asyncio.TimeoutError: continue - request_info.scheduler_node_id = self.messaging.worker_index + request_info.scheduler_node_id = self.messaging.worker_index or -1 request_info.error = "Request was cancelled" - request_info.scheduler_timings.resolve_end = time.time() + request_info.timings.resolve_end = time.time() self._send_update("cancelled", None, request, request_info) - async def _process_next_request(self): + async def _process_next_request(self, target_start: float): + """ + Process a single request from queue to completion. + + Retrieves request from messaging queue, applies timing strategy, processes + through backend, and publishes status updates throughout the lifecycle. + + :param target_start: Unix timestamp when request should begin processing + """ request: RequestT | MultiTurnRequestT[RequestT] | None = None - request_info: ScheduledRequestInfo | None = None + request_info: RequestInfo | None = None response: ResponseT | None = None try: - # Pull request from the queue + # Pull request from the queue, update state, and send "pending" update request, request_info = await self.messaging.get() + request_info.timings.dequeued = time.time() + request_info.scheduler_node_id = self.messaging.worker_index or -1 + request_info.timings.targeted_start = target_start + self._send_update("pending", response, request, request_info) - if isinstance(request, (list, tuple)): + if request is None or request_info is None: + raise RuntimeError("Received invalid request or request info") + if isinstance(request, list | tuple): raise NotImplementedError("Multi-turn requests are not yet supported") - # Calculate targeted start and set pending state for request - request_info.scheduler_node_id = self.messaging.worker_index - request_info.scheduler_timings.dequeued = time.time() - target_start = ( - request_info.scheduler_start_time + self.request_timings.next_offset() - ) - request_info.scheduler_timings.targeted_start = target_start - self._send_update("pending", response, request, request_info) - # Schedule the request current_time = time.time() - request_info.scheduler_timings.scheduled_at = current_time + request_info.timings.scheduled_at = current_time if target_start > current_time: await asyncio.sleep(target_start - current_time) # Adapt delay so that scheduled at reflects the sleep time - request_info.scheduler_timings.scheduled_at = target_start + request_info.timings.scheduled_at = target_start # Process the request with the backend - request_info.scheduler_timings.resolve_start = time.time() + request_info.timings.resolve_start = time.time() self._send_update("in_progress", response, request, request_info) async for resp, info in self.backend.resolve(request, request_info, None): response = resp request_info = info # Complete the request - request_info.scheduler_timings.resolve_end = time.time() + request_info.timings.resolve_end = time.time() self._send_update("completed", response, request, request_info) response = request = request_info = None @@ -346,14 +385,17 @@ async def _process_next_request(self): # Handle cancellation if request is not None and request_info is not None: request_info.error = "Request was cancelled" - request_info.scheduler_timings.resolve_end = time.time() + request_info.timings.resolve_end = time.time() self._send_update("cancelled", response, request, request_info) raise except Exception as exc: # noqa: BLE001 if request is not None and request_info is not None: request_info.error = str(exc) - request_info.scheduler_timings.resolve_end = time.time() + request_info.timings.resolve_end = time.time() self._send_update("errored", response, request, request_info) + finally: + if request_info is not None: + self.strategy.request_completed(request_info) def _send_update( self, @@ -362,8 +404,20 @@ def _send_update( ], response: ResponseT | None, request: RequestT | MultiTurnRequestT[RequestT], - request_info: ScheduledRequestInfo, + request_info: RequestInfo, ): + """ + Publish request status update through messaging system. + + Updates request status and publishes to messaging queue for coordinator + consumption. Prevents duplicate status updates for the same state. + + :param new_status: New status for the request + :param response: Response object if available, None otherwise + :param request: Request object being processed + :param request_info: Request metadata and timing information + :raises Exception: If messaging system fails to publish the update + """ prev_status = request_info.status if new_status == prev_status: diff --git a/src/guidellm/scheduler/worker_group.py b/src/guidellm/scheduler/worker_group.py index c1d516f1..c6027989 100644 --- a/src/guidellm/scheduler/worker_group.py +++ b/src/guidellm/scheduler/worker_group.py @@ -14,7 +14,7 @@ import threading import time import uuid -from collections.abc import AsyncIterator, Generator, Iterable, Iterator +from collections.abc import AsyncIterator, Generator, Iterable from multiprocessing import get_context from multiprocessing.context import BaseContext from multiprocessing.managers import BaseManager @@ -22,19 +22,19 @@ from multiprocessing.synchronize import Barrier, Event from typing import Generic, NamedTuple +from guidellm.logger import logger from guidellm.scheduler.constraints import Constraint, RequestsExhaustedConstraint -from guidellm.scheduler.objects import ( +from guidellm.scheduler.schemas import ( BackendInterface, MultiTurnRequestT, RequestT, ResponseT, - ScheduledRequestInfo, - SchedulerMessagingPydanticRegistry, SchedulerState, SchedulerUpdateAction, ) from guidellm.scheduler.strategies import SchedulingStrategy from guidellm.scheduler.worker import WorkerProcess +from guidellm.schemas import RequestInfo from guidellm.settings import settings from guidellm.utils import ( InterProcessMessaging, @@ -62,7 +62,6 @@ class WorkerProcessGroup(Generic[RequestT, ResponseT]): group = WorkerProcessGroup( requests=request_iterable, - cycle_requests=None, backend=backend_instance, strategy=scheduling_strategy, constraints={"max_time": time_constraint} @@ -81,65 +80,54 @@ class WorkerProcessGroup(Generic[RequestT, ResponseT]): def __init__( self, - requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] | None, - cycle_requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] | None, + requests: Iterable[RequestT | MultiTurnRequestT[RequestT]], backend: BackendInterface[RequestT, ResponseT], strategy: SchedulingStrategy, - constraints: dict[str, Constraint], + startup_duration: float, + **constraints: dict[str, Constraint], ): """ Initialize a worker process group for distributed request processing. :param requests: Finite iterable of requests to process sequentially - :param cycle_requests: Iterable of requests to cycle through indefinitely :param backend: Backend interface for processing requests :param strategy: Scheduling strategy for request timing and distribution + :param startup_duration: Duration in seconds for request startup ramping :param constraints: Named constraints for controlling execution behavior - :raises ValueError: If neither requests nor cycle_requests are provided, - or if cycle_requests is an Iterator rather than Iterable """ - if not requests and not cycle_requests: - raise ValueError( - "At least one of 'requests' or 'cycle_requests' must be provided. " - f"Got requests: {requests}, cycle_requests: {cycle_requests}" - ) - - if isinstance(cycle_requests, Iterator): - raise ValueError( - f"cycle_requests must be an Iterable or None, not an Iterator. " - f"Got {type(cycle_requests)}" - ) - self.requests = requests - self.cycle_requests = cycle_requests self.backend = backend self.strategy = strategy + self.startup_duration = startup_duration self.constraints = constraints # Multiprocessing contexts and primitives, created in create_processes - self.mp_context: BaseContext = None - self.mp_manager: BaseManager = None - self.processes: list[BaseProcess] = None - self.startup_barrier: Barrier = None - self.requests_generated_event: Event = None - self.constraint_reached_event: Event = None - self.shutdown_event: Event = None - self.error_event: Event = None + self.mp_context: BaseContext | None = None + self.mp_manager: BaseManager | None = None + self.processes: list[BaseProcess] | None = None + self.startup_barrier: Barrier | None = None + self.requests_generated_event: Event | None = None + self.constraint_reached_event: Event | None = None + self.shutdown_event: Event | None = None + self.error_event: Event | None = None # Scheduler and messaging state, created in start - self.state: WorkerGroupState[ResponseT, RequestT] = None - self.messaging: InterProcessMessaging[ - tuple[ - RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, - ], - tuple[ - ResponseT | None, - RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, - SchedulerState, - ], - ] = None + self.state: WorkerGroupState[RequestT, ResponseT] | None = None + self.messaging: ( + InterProcessMessaging[ + tuple[ + RequestT | MultiTurnRequestT[RequestT], + RequestInfo, + ], + tuple[ + ResponseT | None, + RequestT | MultiTurnRequestT[RequestT], + RequestInfo, + SchedulerState, + ], + ] + | None + ) = None async def create_processes(self): """ @@ -153,19 +141,23 @@ async def create_processes(self): :raises RuntimeError: If process initialization or startup fails """ # Processes limits and params - max_conc: int = min( - self.strategy.requests_limit or math.inf, - self.backend.requests_limit or math.inf, - ) - if max_conc == math.inf: - # if concurrency not specified, use settings + max_conc: int + if ( + requests_limit := min( + self.strategy.requests_limit or math.inf, + self.backend.requests_limit or math.inf, + ) + ) != math.inf: + max_conc = int(requests_limit) + else: + # If concurrency not specified, use settings max_conc = settings.max_concurrency if max_conc <= 0: raise RuntimeError("max_concurrency resolved to 0; increase limits/config") # Calculate number of processes, ensure we don't exceed the max concurrency, # or limits from the backend, strategy, or user settings - num_processes = int( + num_processes: int = int( min( max_conc, self.strategy.processes_limit or math.inf, @@ -180,12 +172,10 @@ async def create_processes(self): max_pending_size = max( 1, math.floor(max_conc * settings.mp_max_pending_buffer_percent) ) - per_proc_max_buffer_size = max( - 1, math.floor(per_proc_max_conc * settings.mp_max_worker_buffer_percent) - ) + per_proc_max_buffer_size = 1 # Initialize multiprocessing components - self.mp_context: BaseContext = get_context(settings.mp_context_type) + self.mp_context = get_context(settings.mp_context_type) self.mp_manager = self.mp_context.Manager() self.startup_barrier = self.mp_context.Barrier(num_processes + 1) self.requests_generated_event = self.mp_context.Event() @@ -225,6 +215,11 @@ async def create_processes(self): # Initialize worker processes self.processes = [] + self.strategy.init_processes_timings( + worker_count=num_processes, + max_concurrency=max_conc, + startup_duration=self.startup_duration, + ) for rank in range(num_processes): # Distribute any remainder across the first N ranks async_limit = per_proc_max_conc + ( @@ -232,18 +227,16 @@ async def create_processes(self): ) worker = WorkerProcess[RequestT, ResponseT]( + worker_index=rank, messaging=self.messaging.create_worker_copy( worker_index=rank, max_buffer_send_size=None, max_buffer_receive_size=per_proc_max_buffer_size, ), backend=self.backend, - request_timings=self.strategy.create_request_timings( - local_rank=rank, - local_world_size=num_processes, - local_max_concurrency=async_limit, - ), + strategy=self.strategy, async_limit=async_limit, + fut_scheduling_time_limit=0.0, startup_barrier=self.startup_barrier, requests_generated_event=self.requests_generated_event, constraint_reached_event=self.constraint_reached_event, @@ -280,9 +273,17 @@ async def start(self, start_time: float): :raises RuntimeError: If workers encounter errors during startup or if create_processes() was not called first """ - if not self.processes: + if ( + not self.processes + or not self.requests_generated_event + or not self.constraint_reached_event + or not self.shutdown_event + or not self.error_event + or not self.messaging + ): raise RuntimeError("create_processes() must be called before start()") + self.strategy.init_processes_start(start_time=start_time) stop_send_requests_event = threading.Event() send_requests_stopped_event = threading.Event() self.state = WorkerGroupState[RequestT, ResponseT]( @@ -295,16 +296,14 @@ async def start(self, start_time: float): constraint_reached_event=self.constraint_reached_event, shutdown_event=self.shutdown_event, error_event=self.error_event, + messaging=self.messaging, ) await self.messaging.start( - send_items=self.state.requests_generator( - self.requests, self.cycle_requests - ), + send_items=self.state.requests_generator(self.requests), receive_callback=self.state.received_callback, send_stopped_event=send_requests_stopped_event, send_stop_criteria=[stop_send_requests_event], receive_stop_criteria=[self.shutdown_event], - pydantic_models=list(SchedulerMessagingPydanticRegistry.registry.values()), ) if (wait_time := start_time - time.time()) > 0: @@ -320,8 +319,8 @@ async def request_updates( ) -> AsyncIterator[ tuple[ ResponseT | None, - RequestT, - ScheduledRequestInfo, + RequestT | MultiTurnRequestT[RequestT], + RequestInfo, SchedulerState, ] ]: @@ -337,7 +336,8 @@ async def request_updates( :raises RuntimeError: If workers encounter unrecoverable errors """ while True: - if self.error_event.is_set(): + if self.error_event.is_set(): # type: ignore[union-attr] + logger.error("Error event set in WorkerProcessGroup") raise RuntimeError( "error_event is set in WorkerProcessGroup, " "indicating an error occurred in one of the worker processes." @@ -349,11 +349,11 @@ async def request_updates( request, request_info, scheduler_state, - ) = await self.messaging.get(timeout=settings.mp_poll_interval) + ) = await self.messaging.get(timeout=settings.mp_poll_interval) # type: ignore[union-attr] yield response, request, request_info, scheduler_state except asyncio.TimeoutError: - if self.shutdown_event.is_set(): + if self.shutdown_event.is_set(): # type: ignore[union-attr] # Everything yielded, exit break @@ -411,6 +411,8 @@ async def shutdown(self) -> list[Exception]: # noqa: C901 class _StateUpdate(NamedTuple): + """Internal state update result with control flags.""" + state: SchedulerState stop_queueing: bool stop_processing: bool @@ -436,6 +438,15 @@ def __init__( constraint_reached_event: Event, shutdown_event: Event, error_event: Event, + messaging: InterProcessMessaging[ + tuple[RequestT | MultiTurnRequestT[RequestT], RequestInfo], + tuple[ + ResponseT | None, + RequestT | MultiTurnRequestT[RequestT], + RequestInfo, + SchedulerState, + ], + ], ): """ Initialize worker group state management. @@ -443,6 +454,7 @@ def __init__( :param start_time: Unix timestamp when processing should begin :param processes: List of worker process instances :param constraints: Named constraints for controlling execution behavior + :param stop_send_requests_event: Threading event for stopping request generation :param send_requests_stopped_event: Threading event for request coordination :param requests_generated_event: Multiprocessing event for generation completion :param constraint_reached_event: Multiprocessing event for constraint stopping @@ -458,6 +470,7 @@ def __init__( self.constraint_reached_event = constraint_reached_event self.shutdown_event = shutdown_event self.error_event = error_event + self.messaging = messaging self._update_lock: threading.Lock = threading.Lock() self._state: SchedulerState = SchedulerState( @@ -465,15 +478,15 @@ def __init__( num_processes=len(processes), start_time=start_time, ) - self._queued_requests = set() - self._pending_requests = set() - self._processing_requests = set() + self._queued_requests: set[RequestT | MultiTurnRequestT[RequestT]] = set() + self._pending_requests: set[RequestT | MultiTurnRequestT[RequestT]] = set() + self._processing_requests: set[RequestT | MultiTurnRequestT[RequestT]] = set() def requests_generator( - self, - requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] | None, - cycle_requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] | None, - ) -> Generator[tuple[RequestT | MultiTurnRequestT[RequestT],], None, None]: + self, requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] + ) -> Generator[ + tuple[RequestT | MultiTurnRequestT[RequestT], RequestInfo], None, None + ]: """ Generate request-info pairs for worker processing with constraint evaluation. @@ -482,60 +495,64 @@ def requests_generator( constraints to determine when to stop request generation. :param requests: Finite iterable of requests to process sequentially - :param cycle_requests: Iterable of requests to cycle through indefinitely :return: Generator yielding (request, request_info) tuples """ - def _iter(): - if requests: - yield from requests - - if cycle_requests: - while True: - yield from cycle_requests - - count = 0 - request_info: ScheduledRequestInfo = None - for request in _iter(): - count += 1 - - if hasattr(request, "request_id"): - request_id = request.request_id - elif hasattr(request, "id"): - request_id = request.id - else: - request_id = str(uuid.uuid4()) - request_info: ScheduledRequestInfo = ScheduledRequestInfo( - request_id=request_id, - status="queued", - scheduler_process_id=0, - scheduler_start_time=self.start_time, - ) - state_update = self._locked_update(request_info) - yield (request, request_info) + try: + count = 0 + for request in iter(requests): + count += 1 + + if hasattr(request, "request_id"): + request_id = request.request_id + elif hasattr(request, "id"): + request_id = request.id + else: + request_id = str(uuid.uuid4()) + request_info: RequestInfo = RequestInfo( + request_id=request_id, + status="queued", + scheduler_process_id=0, + scheduler_start_time=self.start_time, + ) + state_update = self._locked_update(request_info) + request_info.timings.queued = time.time() + self.messaging.buffer_receive_queue.sync_put( + (None, request, request_info, state_update.state) + ) - if state_update.stop_queueing: - self.stop_send_requests_event.set() - return + yield (request, request_info) - # Reached the end, inject a RequestsExhaustedConstraint to record - self._locked_update( - info=None, - requests_exhausted=RequestsExhaustedConstraint(num_requests=count), - ) - self.stop_send_requests_event.set() + if state_update.stop_queueing: + self.stop_send_requests_event.set() + return + + # Reached the end, inject a RequestsExhaustedConstraint to record + self._locked_update( + info=None, + requests_exhausted={ + "requests_exhausted": RequestsExhaustedConstraint( + num_requests=count + ) + }, + ) + self.stop_send_requests_event.set() + except Exception as err: + logger.error(f"Error generating requests: {err}") + self.error_event.set() + raise err def received_callback( self, update: tuple[ ResponseT | None, RequestT | MultiTurnRequestT, - ScheduledRequestInfo, + RequestInfo, ], ) -> tuple[ ResponseT | None, RequestT | MultiTurnRequestT, - ScheduledRequestInfo, + RequestInfo, SchedulerState, ]: """ @@ -548,31 +565,40 @@ def received_callback( :param update: Tuple containing response, request, and request info :return: Updated tuple with injected scheduler state """ - response, request, request_info = update - state_update = self._locked_update(info=request_info) + try: + response, request, request_info = update + state_update = self._locked_update(info=request_info) - # Check if we need to tell workers to stop pulling new requests - # based on no more requests sent and all requests removed from queue - if ( - state_update.state.queued_requests == 0 - and self.send_requests_stopped_event.is_set() - and not self.requests_generated_event.is_set() - ): - self.requests_generated_event.set() + # Check if we need to tell workers to stop pulling new requests + # based on no more requests sent and all requests removed from queue + if ( + state_update.state.queued_requests == 0 + and self.stop_send_requests_event.is_set() + and not self.requests_generated_event.is_set() + ): + self.requests_generated_event.set() - # Check if we need to tell workers to stop processing requests (constraints) - if state_update.stop_processing and not self.constraint_reached_event.is_set(): - self.constraint_reached_event.set() + # Check if we need to tell workers to stop processing requests (constraints) + if ( + state_update.stop_processing + and not self.constraint_reached_event.is_set() + ): + self.constraint_reached_event.set() - # Check if all requests have been processed and can shutdown - if ( - state_update.state.processed_requests == state_update.state.created_requests - and self.send_requests_stopped_event.is_set() - and self.requests_generated_event.is_set() - and self.constraint_reached_event.is_set() - and not self.shutdown_event.is_set() - ): - self.shutdown_event.set() + # Check if all requests have been processed and can shutdown + if ( + state_update.state.processed_requests + == state_update.state.created_requests + and self.stop_send_requests_event.is_set() + and self.requests_generated_event.is_set() + and self.constraint_reached_event.is_set() + and not self.shutdown_event.is_set() + ): + self.shutdown_event.set() + except Exception as err: + logger.error(f"Error processing received update: {err}") + self.error_event.set() + raise err return ( response, @@ -583,7 +609,7 @@ def received_callback( def _locked_update( self, - info: ScheduledRequestInfo | None = None, + info: RequestInfo | None = None, **add_constraints: dict[str, Constraint], ) -> _StateUpdate: with self._update_lock: @@ -603,7 +629,7 @@ def _locked_update( state_copy.end_processing_time is not None, ) - def _update_state_request_counts(self, info: ScheduledRequestInfo): + def _update_state_request_counts(self, info: RequestInfo): if info.status == "queued": self._queued_requests.add(info.request_id) self._state.queued_requests = len(self._queued_requests) @@ -640,7 +666,7 @@ def _update_state_request_counts(self, info: ScheduledRequestInfo): else: raise ValueError(f"Unknown request_info status {info.status} for {info}") - def _update_with_constraints(self, info: ScheduledRequestInfo): + def _update_with_constraints(self, info: RequestInfo): actions: dict[str, SchedulerUpdateAction] = { name: const(self._state, info) for name, const in self.constraints.items() } diff --git a/src/guidellm/schemas/__init__.py b/src/guidellm/schemas/__init__.py new file mode 100644 index 00000000..42268f72 --- /dev/null +++ b/src/guidellm/schemas/__init__.py @@ -0,0 +1,31 @@ +""" +Pydantic schema models for GuideLLM operations. + +Provides standardized data models and type definitions for generation requests, +responses, timing measurements, and statistics aggregation. These schemas ensure +type safety and consistent data handling across the benchmarking pipeline, +from request submission through backend processing to results compilation. +""" + +from __future__ import annotations + +from .info import RequestInfo, RequestTimings +from .request import ( + GenerationRequest, + GenerationRequestArguments, + GenerativeRequestType, + UsageMetrics, +) +from .response import GenerationResponse +from .stats import GenerativeRequestStats + +__all__ = [ + "GenerationRequest", + "GenerationRequestArguments", + "GenerationResponse", + "GenerativeRequestStats", + "GenerativeRequestType", + "RequestInfo", + "RequestTimings", + "UsageMetrics", +] diff --git a/src/guidellm/schemas/info.py b/src/guidellm/schemas/info.py new file mode 100644 index 00000000..4b5d188c --- /dev/null +++ b/src/guidellm/schemas/info.py @@ -0,0 +1,159 @@ +""" +Core data structures and interfaces for the GuideLLM scheduler system. + +Provides type-safe abstractions for distributed request processing, timing +measurements, and backend interfaces for benchmarking operations. Central to +the scheduler architecture, enabling request lifecycle tracking, backend +coordination, and state management across distributed worker processes. +""" + +from __future__ import annotations + +import uuid +from typing import Literal + +from pydantic import Field, computed_field + +from guidellm.utils import StandardBaseDict, StandardBaseModel + +__all__ = ["RequestInfo", "RequestTimings"] + + +class RequestTimings(StandardBaseDict): + """ + Timing measurements for tracking request lifecycle events. + + Provides comprehensive timing data for distributed request processing, capturing + key timestamps from initial targeting through final completion. Essential for + performance analysis, SLA monitoring, and debugging request processing bottlenecks + across scheduler workers and backend systems. + """ + + targeted_start: float | None = Field( + default=None, + description="Unix timestamp when request was initially targeted for execution", + ) + queued: float | None = Field( + default=None, + description="Unix timestamp when request was placed into processing queue", + ) + dequeued: float | None = Field( + default=None, + description="Unix timestamp when request was removed from queue for processing", + ) + scheduled_at: float | None = Field( + default=None, + description="Unix timestamp when the request was scheduled for processing", + ) + resolve_start: float | None = Field( + default=None, + description="Unix timestamp when backend resolution of the request began", + ) + request_start: float | None = Field( + default=None, + description="Unix timestamp when the backend began processing the request", + ) + first_iteration: float | None = Field( + default=None, + description="Unix timestamp when the first iteration for a streaming began", + ) + last_iteration: float | None = Field( + default=None, + description="Unix timestamp when the last iteration for a streaming completed", + ) + iterations: int | None = Field( + default=None, + description="Total number of streaming update iterations performed", + ) + request_end: float | None = Field( + default=None, + description="Unix timestamp when the backend completed processing the request", + ) + resolve_end: float | None = Field( + default=None, + description="Unix timestamp when backend resolution of the request completed", + ) + finalized: float | None = Field( + default=None, + description="Unix timestamp when request was processed by the scheduler", + ) + + +class RequestInfo(StandardBaseModel): + """ + Complete information about a request in the scheduler system. + + Encapsulates all metadata, status tracking, and timing information for requests + processed through the distributed scheduler. Provides comprehensive lifecycle + tracking from initial queuing through final completion, including error handling + and node identification for debugging and performance analysis. + + Example: + :: + request = RequestInfo() + request.status = "in_progress" + start_time = request.started_at + completion_time = request.completed_at + """ + + request_id: str = Field( + description="Unique identifier for the request", + default_factory=lambda: str(uuid.uuid4()), + ) + status: Literal[ + "queued", "pending", "in_progress", "completed", "errored", "cancelled" + ] = Field(description="Current processing status of the request", default="queued") + scheduler_node_id: int = Field( + description="ID/rank of the scheduler node handling the request", + default=-1, + ) + scheduler_process_id: int = Field( + description="ID/rank of the node's scheduler process handling the request", + default=-1, + ) + scheduler_start_time: float = Field( + description="Unix timestamp when scheduler processing began", + default=-1, + ) + timings: RequestTimings = Field( + default_factory=RequestTimings, + description="Timing measurements for the request lifecycle", + ) + + error: str | None = Field( + default=None, description="Error message if the request status is 'errored'" + ) + + @computed_field # type: ignore[misc] + @property + def started_at(self) -> float | None: + """ + Get the effective request processing start time. + + :return: Unix timestamp when processing began, or None if not started + """ + return self.timings.request_start or self.timings.resolve_start + + @computed_field # type: ignore[misc] + @property + def completed_at(self) -> float | None: + """ + Get the effective request processing completion time. + + :return: Unix timestamp when processing completed, or None if not completed + """ + return self.timings.request_end or self.timings.resolve_end + + def model_copy(self, **_kwargs) -> RequestInfo: # type: ignore[override] # noqa: ARG002 + """ + Create a deep copy of the request info with copied timing objects. + + :param kwargs: Additional keyword arguments for model copying + :return: New RequestInfo instance with independent timing objects + """ + return super().model_copy( + update={ + "timings": self.timings.model_copy(), + }, + deep=False, + ) diff --git a/src/guidellm/schemas/request.py b/src/guidellm/schemas/request.py new file mode 100644 index 00000000..9e9189fc --- /dev/null +++ b/src/guidellm/schemas/request.py @@ -0,0 +1,216 @@ +""" +Request schema definitions for generation operations. + +Contains request models and data structures used to define and execute generation +requests across different backend services. Provides standardized interfaces for +request arguments, usage metrics tracking, and request type definitions that enable +consistent interaction with various AI generation APIs. +""" + +from __future__ import annotations + +import uuid +from typing import Any, Literal + +from pydantic import Field, computed_field + +from guidellm.utils import StandardBaseDict, StandardBaseModel + +__all__ = [ + "GenerationRequest", + "GenerationRequestArguments", + "GenerativeRequestType", + "UsageMetrics", +] + + +GenerativeRequestType = Literal[ + "text_completions", + "chat_completions", + "audio_transcriptions", + "audio_translations", +] + + +class GenerationRequestArguments(StandardBaseDict): + """ + HTTP request arguments for generation operations. + + Encapsulates all necessary HTTP request components including method, headers, + parameters, and payload data required to execute generation requests against + backend services. Supports file uploads and streaming responses. + """ + + method: str | None = Field( + default=None, + description="The HTTP method to use for the request (e.g., 'POST', 'GET').", + ) + stream: bool | None = Field( + default=None, + description="Whether to stream the response, if applicable.", + ) + headers: dict[str, str] | None = Field( + default=None, + description="Any headers to include in the request, if applicable.", + ) + params: dict[str, Any] | None = Field( + default=None, + description="Query parameters to include in the request, if applicable.", + ) + body: dict[str, Any] | None = Field( + default=None, + description="Content to include in the main request body.", + ) + files: dict[str, Any] | None = Field( + default=None, + description="Files to include in the request, if applicable.", + ) + + def model_combine( + self, additional: GenerationRequestArguments | dict[str, Any] + ) -> GenerationRequestArguments: + """ + Merge additional request arguments into the current instance. + + Combines method and stream fields by overwriting, while merging collection + fields like headers, params, json_body, and files by extending existing values. + + :param additional: Additional arguments to merge with current instance + :return: Updated instance with merged arguments + """ + additional_dict = ( + additional.model_dump() + if isinstance(additional, GenerationRequestArguments) + else additional + ) + + for overwrite in ("method", "stream"): + if (val := additional_dict.get(overwrite)) is not None: + setattr(self, overwrite, val) + + for combine in ("headers", "params", "json_body", "files"): + if (val := additional_dict.get(combine)) is not None: + setattr(self, combine, {**getattr(self, combine, {}), **val}) + + return self + + +class UsageMetrics(StandardBaseDict): + """ + Multimodal usage metrics for generation requests. + + Tracks resource consumption across different modalities including text, images, + video, and audio. Provides granular metrics for tokens, bytes, duration, and + format-specific measurements to enable comprehensive usage monitoring and billing. + """ + + # Text stats + text_tokens: int | None = Field( + default=None, description="Number of text tokens processed/generated." + ) + text_words: int | None = Field( + default=None, description="Number of text words processed/generated." + ) + text_characters: int | None = Field( + default=None, description="Number of text characters processed/generated." + ) + + # Vision image stats + image_tokens: int | None = Field( + default=None, description="Number of image tokens processed/generated." + ) + image_count: int | None = Field( + default=None, description="Number of images processed/generated." + ) + image_pixels: int | None = Field( + default=None, description="Number of image pixels processed/generated." + ) + image_bytes: int | None = Field( + default=None, description="Number of image bytes processed/generated." + ) + + # Vision video stats + video_tokens: int | None = Field( + default=None, description="Number of video tokens processed/generated." + ) + video_frames: int | None = Field( + default=None, description="Number of video frames processed/generated." + ) + video_seconds: float | None = Field( + default=None, description="Duration of video processed/generated in seconds." + ) + video_bytes: int | None = Field( + default=None, description="Number of video bytes processed/generated." + ) + + # Audio stats + audio_tokens: int | None = Field( + default=None, description="Number of audio tokens processed/generated." + ) + audio_samples: int | None = Field( + default=None, description="Number of audio samples processed/generated." + ) + audio_seconds: float | None = Field( + default=None, description="Duration of audio processed/generated in seconds." + ) + audio_bytes: int | None = Field( + default=None, description="Number of audio bytes processed/generated." + ) + + @computed_field # type: ignore[misc] + @property + def total_tokens(self) -> int | None: + """ + Calculate total tokens across all modalities. + + :return: Sum of text, image, video, and audio tokens, or None if all are None + """ + return (self.text_tokens or 0) + (self.image_tokens or 0) + ( + self.video_tokens or 0 + ) + (self.audio_tokens or 0) or None + + +class GenerationRequest(StandardBaseModel): + """ + Complete request specification for backend generation operations. + + Encapsulates all components needed to execute a generation request including + unique identification, request type specification, HTTP arguments, and input/output + usage metrics. Serves as the primary interface between the scheduler and backend + services for coordinating AI generation tasks. + + Example:: + request = GenerationRequest( + request_type="text_completions", + arguments=GenerationRequestArguments( + method="POST", + body={"prompt": "Hello world", "max_tokens": 100} + ) + ) + """ + + request_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique identifier for the request.", + ) + request_type: GenerativeRequestType | str = Field( + description=( + "Type of request. If url is not provided in arguments, " + "this will be used to determine the request url." + ), + ) + arguments: GenerationRequestArguments = Field( + description=( + "Payload for the request, structured as a dictionary of arguments to pass " + "to the respective backend method. For example, can contain " + "'json', 'headers', 'files', etc." + ) + ) + input_metrics: UsageMetrics = Field( + default_factory=UsageMetrics, + description="Input statistics including counts, sizes, and durations.", + ) + output_metrics: UsageMetrics = Field( + default_factory=UsageMetrics, + description="Output statistics including counts, sizes, and durations.", + ) diff --git a/src/guidellm/schemas/response.py b/src/guidellm/schemas/response.py new file mode 100644 index 00000000..d4e53aa3 --- /dev/null +++ b/src/guidellm/schemas/response.py @@ -0,0 +1,119 @@ +""" +Backend response models for request and response handling. + +Provides standardized response models for generation operations that capture +output text, usage metrics, and compilation of request statistics. Ensures +consistent data handling and statistics aggregation across different backend +implementations. +""" + +from __future__ import annotations + +from pydantic import Field + +from guidellm.schemas.info import RequestInfo +from guidellm.schemas.request import GenerationRequest, UsageMetrics +from guidellm.schemas.stats import GenerativeRequestStats +from guidellm.utils import StandardBaseModel + +__all__ = ["GenerationResponse"] + + +class GenerationResponse(StandardBaseModel): + """ + Response model for backend generation operations. + + Captures the output and metrics from a generation request, providing structured + data for text output, token usage statistics, and compilation of detailed + request statistics for analysis and monitoring purposes. + + Example: + :: + response = GenerationResponse( + request_id="req-123", + text="Generated response text", + input_metrics=UsageMetrics(token_count=50), + output_metrics=UsageMetrics(token_count=25) + ) + stats = response.compile_stats(request, info) + """ + + request_id: str = Field( + description="Unique identifier matching the original GenerationRequest." + ) + request_args: str | None = Field( + description="Arguments passed to the backend for request processing." + ) + text: str | None = Field( + default=None, + description="The generated response text.", + ) + input_metrics: UsageMetrics = Field( + default_factory=UsageMetrics, + description="Token usage statistics from the input prompt.", + ) + output_metrics: UsageMetrics = Field( + default_factory=UsageMetrics, + description="Token usage statistics from the generated output.", + ) + + def compile_stats( + self, + request: GenerationRequest, + info: RequestInfo, + prefer_response: bool = True, + ) -> GenerativeRequestStats: + """ + Compile and return comprehensive request statistics. + + Merges metrics from the request and response objects to create a complete + statistical record, with preference given to response-level metrics when + available to ensure accuracy of actual execution data. + + :param request: The original generation request containing input data + :param info: Metadata and timing information for the request execution + :param prefer_response: Whether to prefer response metrics over request + metrics when both are available + :return: A GenerativeRequestStats object containing detailed statistics + :raises ValueError: When request IDs don't match between objects + """ + if request.request_id != self.request_id: + raise ValueError("Mismatched request IDs between request and response.") + + if info.request_id != self.request_id: + raise ValueError("Mismatched request IDs between info and response.") + + if info.status != "completed": + # clear out request output metrics if the request failed since + # those are not valid + request.output_metrics = UsageMetrics() + + base_input = request.input_metrics if prefer_response else self.input_metrics + override_input = ( + self.input_metrics if prefer_response else request.input_metrics + ) + base_output = request.output_metrics if prefer_response else self.output_metrics + override_output = ( + self.output_metrics if prefer_response else request.output_metrics + ) + + input_metrics_dict = base_input.model_dump() + for key, value in override_input.model_dump().items(): + if value is not None: + input_metrics_dict[key] = value + output_metrics_dict = base_output.model_dump() + for key, value in override_output.model_dump().items(): + if value is not None: + output_metrics_dict[key] = value + + return GenerativeRequestStats( + request_id=self.request_id, + request_type=request.request_type, + request_args=str( + request.arguments.model_dump() if request.arguments else {} + ), + output=self.text, + info=info, + input_metrics=UsageMetrics(**input_metrics_dict), + output_metrics=UsageMetrics(**output_metrics_dict), + ) diff --git a/src/guidellm/schemas/stats.py b/src/guidellm/schemas/stats.py new file mode 100644 index 00000000..67f1d26c --- /dev/null +++ b/src/guidellm/schemas/stats.py @@ -0,0 +1,228 @@ +""" +Request statistics and metrics for generative AI benchmark analysis. + +Provides data structures for capturing and analyzing performance metrics from +generative AI workloads. Contains request-level statistics including token counts, +latency measurements, and throughput calculations for text generation benchmarks. +""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import Field, computed_field + +from guidellm.schemas.info import RequestInfo +from guidellm.schemas.request import GenerativeRequestType, UsageMetrics +from guidellm.utils import StandardBaseDict + +__all__ = ["GenerativeRequestStats"] + + +class GenerativeRequestStats(StandardBaseDict): + """ + Request statistics for generative AI text generation workloads. + + Captures comprehensive performance metrics for individual generative requests, + including token counts, timing measurements, and derived performance statistics. + Provides computed properties for latency analysis, throughput calculations, + and token generation metrics essential for benchmark evaluation. + + Example: + :: + stats = GenerativeRequestStats( + request_id="req_123", + request_type="text_completion", + info=request_info, + input_metrics=input_usage, + output_metrics=output_usage + ) + throughput = stats.output_tokens_per_second + """ + + type_: Literal["generative_request_stats"] = "generative_request_stats" + request_id: str = Field(description="Unique identifier for the request") + request_type: GenerativeRequestType | str = Field( + description="Type of generative request: text or chat completion" + ) + request_args: str | None = Field( + default=None, description="Arguments passed to the backend for this request" + ) + output: str | None = Field( + description="Generated text output, if request completed successfully" + ) + info: RequestInfo = Field( + description="Metadata and timing information for the request" + ) + input_metrics: UsageMetrics = Field( + description="Usage statistics for the input prompt" + ) + output_metrics: UsageMetrics = Field( + description="Usage statistics for the generated output" + ) + + # Request stats + @computed_field # type: ignore[misc] + @property + def request_latency(self) -> float | None: + """ + End-to-end request processing latency in seconds. + + :return: Duration from request start to completion, or None if unavailable. + """ + if not self.info.timings.request_end or not self.info.timings.request_start: + return None + + return self.info.timings.request_end - self.info.timings.request_start + + # General token stats + @computed_field # type: ignore[misc] + @property + def prompt_tokens(self) -> int | None: + """ + Number of tokens in the input prompt. + + :return: Input prompt token count, or None if unavailable. + """ + return self.input_metrics.text_tokens + + @computed_field # type: ignore[misc] + @property + def input_tokens(self) -> int | None: + """ + Number of tokens in the input prompt. + + :return: Input prompt token count, or None if unavailable. + """ + return self.input_metrics.total_tokens + + @computed_field # type: ignore[misc] + @property + def output_tokens(self) -> int | None: + """ + Number of tokens in the generated output. + + :return: Generated output token count, or None if unavailable. + """ + return self.output_metrics.total_tokens + + @computed_field # type: ignore[misc] + @property + def total_tokens(self) -> int | None: + """ + Total token count including prompt and output tokens. + + :return: Sum of prompt and output tokens, or None if either is unavailable. + """ + input_tokens = self.input_metrics.total_tokens + output_tokens = self.output_metrics.total_tokens + + if input_tokens is None and output_tokens is None: + return None + + return (input_tokens or 0) + (output_tokens or 0) + + @computed_field # type: ignore[misc] + @property + def time_to_first_token_ms(self) -> float | None: + """ + Time to first token generation in milliseconds. + + :return: Latency from request start to first token, or None if unavailable. + """ + if ( + not self.info.timings.first_iteration + or not self.info.timings.request_start + or self.info.timings.first_iteration == self.info.timings.last_iteration + ): + return None + + return 1000 * ( + self.info.timings.first_iteration - self.info.timings.request_start + ) + + @computed_field # type: ignore[misc] + @property + def time_per_output_token_ms(self) -> float | None: + """ + Average time per output token in milliseconds. + + Includes time for first token and all subsequent tokens. + + :return: Average milliseconds per output token, or None if unavailable. + """ + if ( + not self.info.timings.request_start + or not self.info.timings.last_iteration + or not self.output_metrics.total_tokens + ): + return None + + return ( + 1000 + * (self.info.timings.last_iteration - self.info.timings.request_start) + / self.output_metrics.total_tokens + ) + + @computed_field # type: ignore[misc] + @property + def inter_token_latency_ms(self) -> float | None: + """ + Average inter-token latency in milliseconds. + + Measures time between token generations, excluding first token. + + :return: Average milliseconds between tokens, or None if unavailable. + """ + if ( + not self.info.timings.first_iteration + or not self.info.timings.last_iteration + or not self.output_metrics.total_tokens + or self.output_metrics.total_tokens <= 1 + ): + return None + + return ( + 1000 + * (self.info.timings.last_iteration - self.info.timings.first_iteration) + / (self.output_metrics.total_tokens - 1) + ) + + @computed_field # type: ignore[misc] + @property + def tokens_per_second(self) -> float | None: + """ + Overall token throughput including prompt and output tokens. + + :return: Total tokens per second, or None if unavailable. + """ + if not (latency := self.request_latency) or self.total_tokens is None: + return None + + return self.total_tokens / latency + + @computed_field # type: ignore[misc] + @property + def output_tokens_per_second(self) -> float | None: + """ + Output token generation throughput. + + :return: Output tokens per second, or None if unavailable. + """ + if not (latency := self.request_latency) or self.output_tokens is None: + return None + + return self.output_tokens / latency + + @computed_field # type: ignore[misc] + @property + def output_tokens_per_iteration(self) -> float | None: + """ + Average output tokens generated per iteration. + + :return: Output tokens per iteration, or None if unavailable. + """ + if self.output_tokens is None or not self.info.timings.iterations: + return None + + return self.output_tokens / self.info.timings.iterations diff --git a/src/guidellm/settings.py b/src/guidellm/settings.py index 20d9ff96..f03b19e2 100644 --- a/src/guidellm/settings.py +++ b/src/guidellm/settings.py @@ -32,8 +32,8 @@ class Environment(str, Enum): ENV_REPORT_MAPPING = { - Environment.PROD: "https://blog.vllm.ai/guidellm/ui/latest/index.html", - Environment.STAGING: "https://blog.vllm.ai/guidellm/ui/release/latest/index.html", + Environment.PROD: "https://blog.vllm.ai/guidellm/ui/v0.3.0/index.html", + Environment.STAGING: "https://blog.vllm.ai/guidellm/ui/release/v0.3.0/index.html", Environment.DEV: "https://blog.vllm.ai/guidellm/ui/dev/index.html", Environment.LOCAL: "http://localhost:3000/index.html", } @@ -89,6 +89,10 @@ class OpenAISettings(BaseModel): base_url: str = "http://localhost:8000" max_output_tokens: int = 16384 verify: bool = True + max_output_key: dict[Literal["text_completions", "chat_completions"], str] = { + "text_completions": "max_tokens", + "chat_completions": "max_completion_tokens", + } class ReportGenerationSettings(BaseModel): diff --git a/src/guidellm/utils/__init__.py b/src/guidellm/utils/__init__.py index 702b2a9d..89312771 100644 --- a/src/guidellm/utils/__init__.py +++ b/src/guidellm/utils/__init__.py @@ -17,13 +17,9 @@ safe_getattr, safe_multiply, ) -from .hf_datasets import ( - SUPPORTED_TYPES, - save_dataset_to_file, -) -from .hf_transformers import ( - check_load_processor, -) +from .hf_datasets import SUPPORTED_TYPES, save_dataset_to_file +from .hf_transformers import check_load_processor +from .imports import json from .messaging import ( InterProcessMessaging, InterProcessMessagingManagerQueue, @@ -113,6 +109,7 @@ "format_value_display", "get_literal_vals", "is_punctuation", + "json", "load_text", "recursive_key_update", "safe_add", diff --git a/src/guidellm/utils/cli.py b/src/guidellm/utils/cli.py index 4d83526a..a75c37a8 100644 --- a/src/guidellm/utils/cli.py +++ b/src/guidellm/utils/cli.py @@ -3,10 +3,31 @@ import click +__all__ = ["Union", "format_list_arg", "parse_json", "set_if_not_default"] + def parse_json(ctx, param, value): # noqa: ARG001 - if value is None: + if value is None or value == [None]: return None + if isinstance(value, list | tuple): + return [parse_json(ctx, param, val) for val in value] + + if "{" not in value and "}" not in value and "=" in value: + # Treat it as a key=value pair if it doesn't look like JSON. + result = {} + for pair in value.split(","): + if "=" not in pair: + raise click.BadParameter( + f"{param.name} must be a valid JSON string or key=value pairs." + ) + key, val = pair.split("=", 1) + result[key.strip()] = val.strip() + return result + + if "{" not in value and "}" not in value: + # Treat it as a plain string if it doesn't look like JSON. + return value + try: return json.loads(value) except json.JSONDecodeError as err: @@ -26,6 +47,29 @@ def set_if_not_default(ctx: click.Context, **kwargs) -> dict[str, Any]: return values +def format_list_arg( + value: Any, default: Any = None, simplify_single: bool = False +) -> list[Any] | Any: + """ + Format a multi-argument value for display. + + :param value: The value to format, which can be a single value or a list/tuple. + :param default: The default value to set if the value is non truthy. + :param simplify_single: If True and the value is a single-item list/tuple, + return the single item instead of a list. + :return: Formatted list of values, or single value if simplify_single and applicable + """ + if not value: + return default + + if isinstance(value, tuple): + value = list(value) + elif not isinstance(value, list): + value = [value] + + return value if not simplify_single or len(value) != 1 else value[0] + + class Union(click.ParamType): """ A custom click parameter type that allows for multiple types to be accepted. diff --git a/src/guidellm/utils/encoding.py b/src/guidellm/utils/encoding.py index 6823fb77..50e7dce0 100644 --- a/src/guidellm/utils/encoding.py +++ b/src/guidellm/utils/encoding.py @@ -10,9 +10,8 @@ from __future__ import annotations -import json from collections.abc import Mapping -from typing import Annotated, Any, ClassVar, Generic, Literal, Optional, TypeVar, cast +from typing import Any, ClassVar, Generic, Literal, TypeVar, cast try: import msgpack # type: ignore[import-untyped] # Optional dependency @@ -24,28 +23,22 @@ HAS_MSGPACK = False try: - from msgspec.msgpack import ( # type: ignore[import-not-found] # Optional dependency - Decoder as MsgspecDecoder, + from msgspec.msgpack import ( + Decoder as MsgspecDecoder, # type: ignore[import-not-found] # Optional dependency ) - from msgspec.msgpack import ( # type: ignore[import-not-found] # Optional dependency - Encoder as MsgspecEncoder, + from msgspec.msgpack import ( + Encoder as MsgspecEncoder, # type: ignore[import-not-found] # Optional dependency ) HAS_MSGSPEC = True except ImportError: - MsgspecDecoder = MsgspecEncoder = None + MsgspecDecoder = MsgspecEncoder = None # type: ignore[misc, assignment] # HAS_MSGSPEC will be checked at runtime HAS_MSGSPEC = False -try: - import orjson # type: ignore[import-not-found] # Optional dependency - - HAS_ORJSON = True -except ImportError: - orjson = None - HAS_ORJSON = False from pydantic import BaseModel -from typing_extensions import TypeAlias + +from guidellm.utils.imports import json __all__ = [ "Encoder", @@ -60,14 +53,10 @@ ObjT = TypeVar("ObjT") MsgT = TypeVar("MsgT") -SerializationTypesAlias: TypeAlias = Annotated[ - Optional[Literal["dict", "sequence"]], - "Type alias for available serialization strategies", -] -EncodingTypesAlias: TypeAlias = Annotated[ - Optional[Literal["msgpack", "msgspec"]], - "Type alias for available binary encoding formats", -] +# Type alias for available serialization strategies +SerializationTypesAlias = Literal["dict", "sequence"] | None +# "Type alias for available binary encoding formats" +EncodingTypesAlias = Literal["msgpack", "msgspec"] | None class MessageEncoding(Generic[ObjT, MsgT]): @@ -405,7 +394,7 @@ def to_dict(self, obj: Any) -> Any: if isinstance(obj, BaseModel): return self.to_dict_pydantic(obj) - if isinstance(obj, (list, tuple)) and any( + if isinstance(obj, list | tuple) and any( isinstance(item, BaseModel) for item in obj ): return [ @@ -432,7 +421,7 @@ def from_dict(self, data: Any) -> Any: :param data: Dictionary representation possibly containing type metadata :return: Reconstructed object with proper types restored """ - if isinstance(data, (list, tuple)): + if isinstance(data, list | tuple): return [ self.from_dict_pydantic(item) if isinstance(item, dict) and "*PYD*" in item @@ -493,7 +482,7 @@ def to_sequence(self, obj: Any) -> str | Any: if isinstance(obj, BaseModel): payload_type = "pydantic" payload = self.to_sequence_pydantic(obj) - elif isinstance(obj, (list, tuple)) and any( + elif isinstance(obj, list | tuple) and any( isinstance(item, BaseModel) for item in obj ): payload_type = "collection_sequence" @@ -515,7 +504,7 @@ def to_sequence(self, obj: Any) -> str | Any: ): payload_type = "collection_mapping" keys = ",".join(str(key) for key in obj) - payload = keys.encode() + b"|" if HAS_ORJSON else keys + "|" + payload = keys.encode() + b"|" for item in obj.values(): is_pydantic = isinstance(item, BaseModel) payload = self.pack_next_sequence( @@ -606,15 +595,7 @@ def to_sequence_pydantic(self, obj: BaseModel) -> str | bytes: class_module: str = obj.__class__.__module__ json_data = obj.__pydantic_serializer__.to_json(obj) - return ( - (class_name.encode() + b"|" + class_module.encode() + b"|" + json_data) - if HAS_ORJSON - else ( - class_name + "|" + class_module + "|" + json_data.decode() - if isinstance(json_data, bytes) - else json_data - ) - ) + return class_name.encode() + b"|" + class_module.encode() + b"|" + json_data def from_sequence_pydantic(self, data: str | bytes) -> BaseModel: """ @@ -648,7 +629,7 @@ def to_sequence_python(self, obj: Any) -> str | bytes: :param obj: Python object to serialize :return: JSON string or bytes representation """ - return orjson.dumps(obj) if HAS_ORJSON else json.dumps(obj) + return json.dumps(obj) def from_sequence_python(self, data: str | bytes) -> Any: """ @@ -656,13 +637,7 @@ def from_sequence_python(self, data: str | bytes) -> Any: :param data: JSON string or bytes to deserialize :return: Reconstructed Python object - :raises ImportError: If orjson is required but not available """ - if isinstance(data, bytes): - if not HAS_ORJSON: - raise ImportError("orjson is not available, cannot deserialize bytes") - return orjson.loads(data) - return json.loads(data) def pack_next_sequence( # noqa: C901, PLR0912 @@ -694,33 +669,36 @@ def pack_next_sequence( # noqa: C901, PLR0912 length=(payload_len.bit_length() + 7) // 8 if payload_len > 0 else 1, byteorder="big", ) - if type_ == "pydantic": - payload_type = b"P" - elif type_ == "python": - payload_type = b"p" - elif type_ == "collection_tuple": - payload_type = b"T" - elif type_ == "collection_sequence": - payload_type = b"S" - elif type_ == "collection_mapping": - payload_type = b"M" - else: - raise ValueError(f"Unknown type for packing: {type_}") + match type_: + case "pydantic": + payload_type = b"P" + case "python": + payload_type = b"p" + case "collection_tuple": + payload_type = b"T" + case "collection_sequence": + payload_type = b"S" + case "collection_mapping": + payload_type = b"M" + case _: + raise ValueError(f"Unknown type for packing: {type_}") delimiter = b"|" else: payload_len_output = str(payload_len) - if type_ == "pydantic": - payload_type = "P" - elif type_ == "python": - payload_type = "p" - elif type_ == "collection_tuple": - payload_type = "T" - elif type_ == "collection_sequence": - payload_type = "S" - elif type_ == "collection_mapping": - payload_type = "M" - else: - raise ValueError(f"Unknown type for packing: {type_}") + + match type_: + case "pydantic": + payload_type = "P" + case "python": + payload_type = "p" + case "collection_tuple": + payload_type = "T" + case "collection_sequence": + payload_type = "S" + case "collection_mapping": + payload_type = "M" + case _: + raise ValueError(f"Unknown type for packing: {type_}") delimiter = "|" # Type ignores because types are enforced at runtime diff --git a/src/guidellm/utils/hf_datasets.py b/src/guidellm/utils/hf_datasets.py index 73e55ebc..86f04485 100644 --- a/src/guidellm/utils/hf_datasets.py +++ b/src/guidellm/utils/hf_datasets.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Union from datasets import Dataset @@ -11,7 +10,7 @@ } -def save_dataset_to_file(dataset: Dataset, output_path: Union[str, Path]) -> None: +def save_dataset_to_file(dataset: Dataset, output_path: str | Path) -> None: """ Saves a HuggingFace Dataset to file in a supported format. diff --git a/src/guidellm/utils/hf_transformers.py b/src/guidellm/utils/hf_transformers.py index 1f2aa1b5..636988c3 100644 --- a/src/guidellm/utils/hf_transformers.py +++ b/src/guidellm/utils/hf_transformers.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from transformers import AutoTokenizer, PreTrainedTokenizerBase # type: ignore[import] @@ -9,15 +9,15 @@ def check_load_processor( - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], + processor: str | Path | PreTrainedTokenizerBase | None, + processor_args: dict[str, Any] | None, error_msg: str, ) -> PreTrainedTokenizerBase: if processor is None: raise ValueError(f"Processor/Tokenizer is required for {error_msg}.") try: - if isinstance(processor, (str, Path)): + if isinstance(processor, str | Path): loaded = AutoTokenizer.from_pretrained( processor, **(processor_args or {}), diff --git a/src/guidellm/utils/imports.py b/src/guidellm/utils/imports.py new file mode 100644 index 00000000..8b7ad5f6 --- /dev/null +++ b/src/guidellm/utils/imports.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +try: + import orjson as json +except ImportError: + import json # type: ignore[no-redef] # Done only after a failure. + + +__all__ = ["json"] diff --git a/src/guidellm/utils/messaging.py b/src/guidellm/utils/messaging.py index 9311259d..f64aef8d 100644 --- a/src/guidellm/utils/messaging.py +++ b/src/guidellm/utils/messaging.py @@ -16,13 +16,13 @@ import threading import time from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Callable, Iterable from multiprocessing.connection import Connection from multiprocessing.context import BaseContext from multiprocessing.managers import SyncManager from multiprocessing.synchronize import Event as ProcessingEvent from threading import Event as ThreadingEvent -from typing import Any, Callable, Generic, Protocol, TypeVar, cast +from typing import Any, Generic, Protocol, TypeVar, cast import culsans from pydantic import BaseModel @@ -420,7 +420,7 @@ def _create_check_stop_callable( stop_events = tuple( item for item in stop_criteria or [] - if isinstance(item, (ThreadingEvent, ProcessingEvent)) + if isinstance(item, ThreadingEvent | ProcessingEvent) ) stop_callbacks = tuple(item for item in stop_criteria or [] if callable(item)) @@ -477,7 +477,7 @@ def __init__( self, mp_context: BaseContext | None = None, serialization: SerializationTypesAlias = "dict", - encoding: EncodingTypesAlias = None, + encoding: EncodingTypesAlias | list[EncodingTypesAlias] = None, max_pending_size: int | None = None, max_buffer_send_size: int | None = None, max_done_size: int | None = None, @@ -668,6 +668,8 @@ def _send_messages_task_thread( # noqa: C901, PLR0912 except (culsans.QueueFull, queue.Full): pass + time.sleep(0) # Yield to other threads + def _receive_messages_task_thread( # noqa: C901 self, receive_callback: Callable[[Any], Any] | None, @@ -721,6 +723,8 @@ def _receive_messages_task_thread( # noqa: C901 except (culsans.QueueFull, queue.Full): pass + time.sleep(0) # Yield to other threads + class InterProcessMessagingManagerQueue( InterProcessMessagingQueue[SendMessageT, ReceiveMessageT] @@ -750,7 +754,7 @@ def __init__( manager: SyncManager, mp_context: BaseContext | None = None, serialization: SerializationTypesAlias = "dict", - encoding: EncodingTypesAlias = None, + encoding: EncodingTypesAlias | list[EncodingTypesAlias] = None, max_pending_size: int | None = None, max_buffer_send_size: int | None = None, max_done_size: int | None = None, @@ -854,7 +858,7 @@ def __init__( num_workers: int, mp_context: BaseContext | None = None, serialization: SerializationTypesAlias = "dict", - encoding: EncodingTypesAlias = None, + encoding: EncodingTypesAlias | list[EncodingTypesAlias] = None, max_pending_size: int | None = None, max_buffer_send_size: int | None = None, max_done_size: int | None = None, diff --git a/src/guidellm/utils/mixins.py b/src/guidellm/utils/mixins.py index b001ff2d..7cf28d00 100644 --- a/src/guidellm/utils/mixins.py +++ b/src/guidellm/utils/mixins.py @@ -91,7 +91,7 @@ def create_info_dict(cls, obj: Any) -> dict[str, Any]: "attributes": ( { key: val - if isinstance(val, (str, int, float, bool, list, dict)) + if isinstance(val, str | int | float | bool | list | dict) else repr(val) for key, val in obj.__dict__.items() if not key.startswith("_") diff --git a/src/guidellm/utils/pydantic_utils.py b/src/guidellm/utils/pydantic_utils.py index 55816ef1..05f5ad81 100644 --- a/src/guidellm/utils/pydantic_utils.py +++ b/src/guidellm/utils/pydantic_utils.py @@ -11,11 +11,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, ClassVar, Generic, TypeVar, cast +from typing import Any, ClassVar, Generic, TypeVar, cast, get_args, get_origin from pydantic import BaseModel, ConfigDict, Field, GetCoreSchemaHandler from pydantic_core import CoreSchema, core_schema -from typing_extensions import get_args, get_origin from guidellm.utils.registry import RegistryMixin diff --git a/src/guidellm/utils/random.py b/src/guidellm/utils/random.py index ceef20b9..6c8f396d 100644 --- a/src/guidellm/utils/random.py +++ b/src/guidellm/utils/random.py @@ -1,6 +1,5 @@ import random from collections.abc import Iterator -from typing import Optional __all__ = ["IntegerRangeSampler"] @@ -9,9 +8,9 @@ class IntegerRangeSampler: def __init__( self, average: int, - variance: Optional[int], - min_value: Optional[int], - max_value: Optional[int], + variance: int | None, + min_value: int | None, + max_value: int | None, random_seed: int, ): self.average = average diff --git a/src/guidellm/utils/registry.py b/src/guidellm/utils/registry.py index e6f1b657..cf450bd1 100644 --- a/src/guidellm/utils/registry.py +++ b/src/guidellm/utils/registry.py @@ -10,7 +10,8 @@ from __future__ import annotations -from typing import Any, Callable, ClassVar, Generic, TypeVar, cast +from collections.abc import Callable +from typing import ClassVar, Generic, TypeVar, cast from guidellm.utils.auto_importer import AutoImporterMixin @@ -64,7 +65,7 @@ class TokenProposal(RegistryMixin): :cvar registry_populated: Track whether auto-discovery has completed """ - registry: ClassVar[dict[str, Any] | None] = None + registry: ClassVar[dict[str, RegistryObjT] | None] = None # type: ignore[misc] registry_auto_discovery: ClassVar[bool] = False registry_populated: ClassVar[bool] = False @@ -103,7 +104,7 @@ def register_decorator( if name is None: name = obj.__name__ - elif not isinstance(name, (str, list)): + elif not isinstance(name, str | list): raise ValueError( "RegistryMixin.register_decorator name must be a string or " f"an iterable of strings. Got {name}." diff --git a/src/guidellm/utils/statistics.py b/src/guidellm/utils/statistics.py index acd9d4f1..a8403c72 100644 --- a/src/guidellm/utils/statistics.py +++ b/src/guidellm/utils/statistics.py @@ -149,7 +149,7 @@ def from_distribution_function( in the output :return: DistributionSummary instance with calculated statistical metrics """ - values, weights = zip(*distribution) if distribution else ([], []) + values, weights = zip(*distribution, strict=True) if distribution else ([], []) values = np.array(values) # type: ignore[assignment] weights = np.array(weights) # type: ignore[assignment] @@ -247,7 +247,7 @@ def from_values( ) return DistributionSummary.from_distribution_function( - distribution=list(zip(values, weights)), + distribution=list(zip(values, weights, strict=True)), include_cdf=include_cdf, ) @@ -255,6 +255,7 @@ def from_values( def from_request_times( requests: list[tuple[float, float]], distribution_type: Literal["concurrency", "rate"], + weights: list[float] | None = None, include_cdf: bool = False, epsilon: float = 1e-6, ) -> DistributionSummary: @@ -273,70 +274,108 @@ def from_request_times( :return: DistributionSummary with timing-based statistical metrics :raises ValueError: If distribution_type is not "concurrency" or "rate" """ - if distribution_type == "concurrency": - # convert to delta changes based on when requests were running - events = [(start, 1) for start, _ in requests] + [ - (end, -1) for _, end in requests - ] - elif distribution_type == "rate": - # convert to events for when requests finished - global_start = min(start for start, _ in requests) if requests else 0 - events = [(global_start, 1)] + [(end, 1) for _, end in requests] - else: - raise ValueError( - f"Invalid distribution_type '{distribution_type}'. " - "Must be 'concurrency' or 'rate'." - ) + if not weights: + weights = [1.0] * len(requests) - # combine any events that are very close together - flattened_events: list[tuple[float, float]] = [] - for time, val in sorted(events): - last_time, last_val = ( - flattened_events[-1] if flattened_events else (None, None) + if len(requests) != len(weights): + raise ValueError( + "The length of requests and weights must be the same.", ) - if ( - last_time is not None - and last_val is not None - and abs(last_time - time) <= epsilon - ): - flattened_events[-1] = (last_time, last_val + val) - else: - flattened_events.append((time, val)) - - if distribution_type == "concurrency": - # convert to the events over time measuring concurrency changes - events_over_time: list[tuple[float, float]] = [] - active = 0 - for time, delta in flattened_events: - active += delta # type: ignore [assignment] - events_over_time.append((time, active)) + # First convert to timing events based on type + events = DistributionSummary._convert_to_timing_events( + requests, distribution_type, weights + ) - flattened_events = events_over_time + # Combine any events within epsilon of each other for stability + flattened_events = DistributionSummary._combine_events(events, epsilon) - # convert to value distribution function + # Convert events to value distribution function distribution: dict[float, float] = defaultdict(float) - for ind in range(len(flattened_events) - 1): - start_time, value = flattened_events[ind] - end_time, _ = flattened_events[ind + 1] - duration = end_time - start_time - - if distribution_type == "concurrency": - # weight the concurrency value by the duration + if distribution_type == "concurrency": + # For concurrency, convert to active concurrency over time + active = 0.0 + for ind in range(len(flattened_events)): + time, change = flattened_events[ind] + active += change + flattened_events[ind] = (time, active) + + # Then convert to distribution by weighting each concurrency + # by duration to next event (last event is 0 concurrency) + for ind in range(len(flattened_events) - 1): + time, value = flattened_events[ind] + next_time = flattened_events[ind + 1][0] + duration = next_time - time distribution[value] += duration - elif distribution_type == "rate": - # weight the rate value by the duration - rate = value / duration + elif distribution_type == "rate": + # For rate, convert to distribution by converting each value + # to a rate (value/duration) weighted by duration from previous + # (first event is 0 rate) + for ind in range(1, len(flattened_events)): + time, value = flattened_events[ind] + prev_time = flattened_events[ind - 1][0] + duration = time - prev_time + rate = value / duration if duration > 0 else 0.0 distribution[rate] += duration - - distribution_list: list[tuple[float, float]] = sorted(distribution.items()) + else: + raise ValueError( + f"Invalid distribution_type '{distribution_type}'. " + "Must be 'concurrency' or 'rate'." + ) return DistributionSummary.from_distribution_function( - distribution=distribution_list, + distribution=sorted(distribution.items()), include_cdf=include_cdf, ) + @staticmethod + def _convert_to_timing_events( + requests: list[tuple[float, float]], + distribution_type: Literal["concurrency", "rate"], + weights: list[float], + ) -> list[tuple[float, float]]: + events: list[tuple[float, float]] = [] + + if distribution_type == "concurrency": + # For concurrency, each request adds to concurrency at start + # and subtracts at end + for (start, end), weight in zip(requests, weights, strict=False): + events.append((start, weight)) + events.append((end, -1 * weight)) + elif distribution_type == "rate": + # For rate, each request is added at the end time only + global_start = min(start for start, _ in requests) if requests else 0.0 + events.append((global_start, 0.0)) + for (_, end), weight in zip(requests, weights, strict=False): + events.append((end, weight)) + else: + raise ValueError( + f"Invalid distribution_type '{distribution_type}'. " + "Must be 'concurrency' or 'rate'." + ) + return events + + @staticmethod + def _combine_events( + events: list[tuple[float, float]], + epsilon: float, + ) -> list[tuple[float, float]]: + sorted_events = sorted(events, key=lambda event: event[0]) + flattened_events: list[tuple[float, float]] = ( + [sorted_events.pop(0)] if sorted_events else [] + ) + last_time = flattened_events[0][0] if flattened_events else 0.0 + + for time, val in sorted_events: + if abs(time - last_time) <= epsilon: + last_val = flattened_events[-1][1] + flattened_events[-1] = (last_time, last_val + val) + else: + last_time = time + flattened_events.append((time, val)) + return flattened_events + @staticmethod def from_iterable_request_times( requests: list[tuple[float, float]], @@ -389,7 +428,7 @@ def from_iterable_request_times( events[global_end] = 0 for (_, end), first_iter, first_iter_count, total_count in zip( - requests, first_iter_times, first_iter_counts, iter_counts + requests, first_iter_times, first_iter_counts, iter_counts, strict=True ): events[first_iter] += first_iter_count @@ -499,36 +538,36 @@ def from_values( ) _, successful_values, successful_weights = ( - zip(*successful) + zip(*successful, strict=True) if ( successful := list( filter( lambda val: val[0] == "successful", - zip(value_types, values, weights), + zip(value_types, values, weights, strict=True), ) ) ) else ([], [], []) ) _, incomplete_values, incomplete_weights = ( - zip(*incomplete) + zip(*incomplete, strict=True) if ( incomplete := list( filter( lambda val: val[0] == "incomplete", - zip(value_types, values, weights), + zip(value_types, values, weights, strict=True), ) ) ) else ([], [], []) ) _, errored_values, errored_weights = ( - zip(*errored) + zip(*errored, strict=True) if ( errored := list( filter( lambda val: val[0] == "error", - zip(value_types, values, weights), + zip(value_types, values, weights, strict=True), ) ) ) @@ -563,6 +602,7 @@ def from_request_times( request_types: list[Literal["successful", "incomplete", "error"]], requests: list[tuple[float, float]], distribution_type: Literal["concurrency", "rate"], + weights: list[float] | None = None, include_cdf: bool = False, epsilon: float = 1e-6, ) -> StatusDistributionSummary: @@ -603,65 +643,78 @@ def from_request_times( f"Got {len(request_types)} and {len(requests)} instead.", ) - _, successful_requests = ( - zip(*successful) + if weights is None: + weights = [1.0] * len(requests) + + if len(requests) != len(weights): + raise ValueError( + "The length of requests and weights must be the same." + f"Got {len(requests)} and {len(weights)} instead.", + ) + + _, successful_requests, successful_weights = ( + zip(*successful, strict=False) if ( successful := list( filter( lambda val: val[0] == "successful", - zip(request_types, requests), + zip(request_types, requests, weights, strict=False), ) ) ) - else ([], []) + else ([], [], []) ) - _, incomplete_requests = ( - zip(*incomplete) + _, incomplete_requests, incomplete_weights = ( + zip(*incomplete, strict=False) if ( incomplete := list( filter( lambda val: val[0] == "incomplete", - zip(request_types, requests), + zip(request_types, requests, weights, strict=False), ) ) ) - else ([], []) + else ([], [], []) ) - _, errored_requests = ( - zip(*errored) + _, errored_requests, errored_weights = ( + zip(*errored, strict=False) if ( errored := list( filter( lambda val: val[0] == "error", - zip(request_types, requests), + zip(request_types, requests, weights, strict=False), ) ) ) - else ([], []) + else ([], [], []) ) return StatusDistributionSummary( total=DistributionSummary.from_request_times( requests, distribution_type=distribution_type, + weights=weights, include_cdf=include_cdf, epsilon=epsilon, ), successful=DistributionSummary.from_request_times( successful_requests, # type: ignore[arg-type] distribution_type=distribution_type, + weights=successful_weights, # type: ignore[arg-type] include_cdf=include_cdf, epsilon=epsilon, ), incomplete=DistributionSummary.from_request_times( incomplete_requests, # type: ignore[arg-type] distribution_type=distribution_type, + weights=incomplete_weights, # type: ignore[arg-type] include_cdf=include_cdf, epsilon=epsilon, ), errored=DistributionSummary.from_request_times( errored_requests, # type: ignore[arg-type] distribution_type=distribution_type, + weights=errored_weights, # type: ignore[arg-type] include_cdf=include_cdf, epsilon=epsilon, ), @@ -734,7 +787,7 @@ def from_iterable_request_times( successful_iter_counts, successful_first_iter_counts, ) = ( - zip(*successful) + zip(*successful, strict=True) if ( successful := list( filter( @@ -745,6 +798,7 @@ def from_iterable_request_times( first_iter_times, iter_counts, first_iter_counts, + strict=True, ), ) ) @@ -758,7 +812,7 @@ def from_iterable_request_times( incomplete_iter_counts, incomplete_first_iter_counts, ) = ( - zip(*incomplete) + zip(*incomplete, strict=True) if ( incomplete := list( filter( @@ -769,6 +823,7 @@ def from_iterable_request_times( first_iter_times, iter_counts, first_iter_counts, + strict=True, ), ) ) @@ -782,7 +837,7 @@ def from_iterable_request_times( errored_iter_counts, errored_first_iter_counts, ) = ( - zip(*errored) + zip(*errored, strict=True) if ( errored := list( filter( @@ -793,6 +848,7 @@ def from_iterable_request_times( first_iter_times, iter_counts, first_iter_counts, + strict=True, ), ) ) @@ -904,7 +960,7 @@ def __add__(self, value: Any) -> float: :return: Updated mean after adding the value :raises ValueError: If value is not numeric (int or float) """ - if not isinstance(value, (int, float)): + if not isinstance(value, int | float): raise ValueError( f"Value must be an int or float, got {type(value)} instead.", ) @@ -921,7 +977,7 @@ def __iadd__(self, value: Any) -> RunningStats: :return: Self reference for method chaining :raises ValueError: If value is not numeric (int or float) """ - if not isinstance(value, (int, float)): + if not isinstance(value, int | float): raise ValueError( f"Value must be an int or float, got {type(value)} instead.", ) diff --git a/src/guidellm/utils/synchronous.py b/src/guidellm/utils/synchronous.py index 64c14e94..d37daec2 100644 --- a/src/guidellm/utils/synchronous.py +++ b/src/guidellm/utils/synchronous.py @@ -16,9 +16,6 @@ from multiprocessing.synchronize import Event as ProcessingEvent from threading import Barrier as ThreadingBarrier from threading import Event as ThreadingEvent -from typing import Annotated, Union - -from typing_extensions import TypeAlias __all__ = [ "SyncObjectTypesAlias", @@ -28,10 +25,10 @@ ] -SyncObjectTypesAlias: TypeAlias = Annotated[ - Union[ThreadingEvent, ProcessingEvent, ThreadingBarrier, ProcessingBarrier], - "Type alias for threading and multiprocessing synchronization object types", -] +# Type alias for threading and multiprocessing synchronization object types +SyncObjectTypesAlias = ( + ThreadingEvent | ProcessingEvent | ThreadingBarrier | ProcessingBarrier +) async def wait_for_sync_event( @@ -146,7 +143,7 @@ async def wait_for_sync_objects( tasks = [ asyncio.create_task( wait_for_sync_barrier(obj, poll_interval) - if isinstance(obj, (ThreadingBarrier, ProcessingBarrier)) + if isinstance(obj, ThreadingBarrier | ProcessingBarrier) else wait_for_sync_event(obj, poll_interval) ) for obj in objects diff --git a/src/guidellm/utils/text.py b/src/guidellm/utils/text.py index a659ac6a..37f2e8d3 100644 --- a/src/guidellm/utils/text.py +++ b/src/guidellm/utils/text.py @@ -13,7 +13,6 @@ import gzip import re import textwrap -from importlib.resources import as_file, files # type: ignore[attr-defined] from pathlib import Path from typing import Any @@ -21,7 +20,6 @@ import httpx from loguru import logger -from guidellm import data as package_data from guidellm.settings import settings from guidellm.utils.console import Colors @@ -239,15 +237,6 @@ def load_text(data: str | Path, encoding: str | None = None) -> str: response.raise_for_status() return response.text - # check package data - if isinstance(data, str) and data.startswith("data:"): - resource_path = files(package_data).joinpath(data[5:]) - with ( - as_file(resource_path) as resource_file, - gzip.open(resource_file, "rt", encoding=encoding) as file, - ): - return file.read() - # check gzipped files if isinstance(data, str) and data.endswith(".gz"): with gzip.open(data, "rt", encoding=encoding) as file: diff --git a/src/guidellm/utils/typing.py b/src/guidellm/utils/typing.py index 8146ea1e..8d3580ef 100644 --- a/src/guidellm/utils/typing.py +++ b/src/guidellm/utils/typing.py @@ -1,14 +1,9 @@ from __future__ import annotations from collections.abc import Iterator +from types import UnionType from typing import Annotated, Literal, Union, get_args, get_origin -# Backwards compatibility for Python <3.10 -try: - from types import UnionType # type: ignore[attr-defined] -except ImportError: - UnionType = Union - # Backwards compatibility for Python <3.12 try: from typing import TypeAliasType # type: ignore[attr-defined] diff --git a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx index 0d804f5c..8f5e3aed 100644 --- a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx +++ b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx @@ -54,7 +54,7 @@ export const Component = () => { const { ttft: ttftSLO, - tpot: tpotSLO, + itl: itlSLO, timePerRequest: timePerRequestSLO, throughput: throughputSLO, percentile, @@ -62,7 +62,7 @@ export const Component = () => { maxX, errors, handleTtft, - handleTpot, + handleItl, handleTimePerRequest, handleThroughput, handlePercentileChange, @@ -72,8 +72,8 @@ export const Component = () => { const isTtftMatch = Boolean( ttftSLO && interpolatedMetricData.ttft.enforcedPercentileValue <= ttftSLO ); - const isTpotMatch = Boolean( - tpotSLO && interpolatedMetricData.tpot.enforcedPercentileValue <= tpotSLO + const isItlMatch = Boolean( + itlSLO && interpolatedMetricData.itl.enforcedPercentileValue <= itlSLO ); const isTprMatch = Boolean( timePerRequestSLO && @@ -123,7 +123,7 @@ export const Component = () => { { @@ -212,7 +212,7 @@ export const Component = () => { { diff --git a/src/ui/lib/components/MetricsSummary/useSummary.ts b/src/ui/lib/components/MetricsSummary/useSummary.ts index 0a6f550c..3046fcb9 100644 --- a/src/ui/lib/components/MetricsSummary/useSummary.ts +++ b/src/ui/lib/components/MetricsSummary/useSummary.ts @@ -13,7 +13,7 @@ type Errors = { [key: string]: string | undefined }; const initErrorsState: Errors = { ttft: undefined, - tpot: undefined, + itl: undefined, timePerRequest: undefined, throughput: undefined, }; @@ -47,20 +47,20 @@ export const useSummary = () => { const dispatch = useDispatch(); const { current, enforcedPercentile, tasksDefaults } = useSelector(selectSloState); - const { ttft, tpot, timePerRequest, throughput } = useSelector( + const { ttft, itl, timePerRequest, throughput } = useSelector( selectMetricsSummaryLineData ); const [errors, setErrors] = useState(initErrorsState); const ttftLimits = findMinMax(ttft || []); - const tpotLimits = findMinMax(tpot || []); + const itlLimits = findMinMax(itl || []); const timePerRequestLimits = findMinMax(timePerRequest || []); const throughputLimits = findMinMax(throughput || []); const limitsByMetric = { ttft: ttftLimits, - tpot: tpotLimits, + itl: itlLimits, timePerRequest: timePerRequestLimits, throughput: throughputLimits, }; @@ -112,7 +112,7 @@ export const useSummary = () => { maxX: ttftLimits.maxX, errors, handleTtft: handleChange('ttft'), - handleTpot: handleChange('tpot'), + handleItl: handleChange('itl'), handleTimePerRequest: handleChange('timePerRequest'), handleThroughput: handleChange('throughput'), handlePercentileChange, diff --git a/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx b/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx index ac333982..e7e632ca 100644 --- a/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx +++ b/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx @@ -36,14 +36,14 @@ export const leftColumn3 = (rpsValue: number, value: number, units: string) => ( export const Component = () => { const { data } = useGetBenchmarksQuery(); - const { ttft, tpot, timePerRequest, throughput } = useSelector( + const { ttft, itl, timePerRequest, throughput } = useSelector( selectMetricsDetailsLineData ); const { currentRequestRate } = useSelector(selectSloState); const formattedRequestRate = formatNumber(currentRequestRate); const { ttft: ttftAtRPS, - tpot: tpotAtRPS, + itl: itlAtRPS, timePerRequest: timePerRequestAtRPS, throughput: throughputAtRPS, } = useSelector(selectInterpolatedMetrics); @@ -57,7 +57,7 @@ export const Component = () => { { )} rightColumn={columnContent(formattedRequestRate, ttftAtRPS.percentiles, 'ms')} > - + - + @@ -99,7 +99,7 @@ export const Component = () => { { 's' )} > - + diff --git a/src/ui/lib/store/benchmarksWindowData.ts b/src/ui/lib/store/benchmarksWindowData.ts index a589e8ed..87faf7bc 100644 --- a/src/ui/lib/store/benchmarksWindowData.ts +++ b/src/ui/lib/store/benchmarksWindowData.ts @@ -1,7 +1,7 @@ export const benchmarksScript = `window.benchmarks = [ { requestsPerSecond: 11.411616848282272, - tpot: { + itl: { mean: 8.758024845683707, median: 8.788176945277623, mode: 7.119315011160714, @@ -172,7 +172,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 36.289181300710815, - tpot: { + itl: { mean: 588.0161376137819, median: 461.7137227739607, mode: 323.1611592429025, @@ -343,7 +343,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 20.752070927855794, - tpot: { + itl: { mean: 116.28360712595156, median: 26.769569941929408, mode: 10.624987738473076, @@ -514,7 +514,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 26.81917480361788, - tpot: { + itl: { mean: 299.7306064613554, median: 372.7384294782366, mode: 13.360295976911273, @@ -685,7 +685,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 26.823988819498975, - tpot: { + itl: { mean: 683.8011571339198, median: 742.2689029148647, mode: 317.1694278717041, @@ -856,7 +856,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 24.50047903792646, - tpot: { + itl: { mean: 742.9258901891964, median: 773.0941431862967, mode: 538.750410079956, @@ -1027,7 +1027,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 25.617829792196602, - tpot: { + itl: { mean: 663.3098317044122, median: 613.7458937508719, mode: 440.9824098859514, @@ -1198,7 +1198,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 37.02892550982192, - tpot: { + itl: { mean: 606.4144710877113, median: 543.5235500335693, mode: 331.6155501774379, @@ -1369,7 +1369,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 37.29183354201869, - tpot: { + itl: { mean: 603.3237551205925, median: 528.1183038439069, mode: 400.96027510506764, @@ -1540,7 +1540,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 37.45318312972309, - tpot: { + itl: { mean: 600.7204526769262, median: 626.2100083487375, mode: 398.7384523664202, diff --git a/src/ui/lib/store/mockData.ts b/src/ui/lib/store/mockData.ts index 8295c60c..2fcb4b8f 100644 --- a/src/ui/lib/store/mockData.ts +++ b/src/ui/lib/store/mockData.ts @@ -95,7 +95,7 @@ export const benchmarks = [ ], bucketWidth: 0, }, - tpot: { + itl: { statistics: { total: 0, mean: 0, diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts index 838dbc7a..efddfc39 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts @@ -45,9 +45,9 @@ const setDefaultSLOs = ( lastBM?.ttft, defaultPercentile ); - const tpotAvg = getAverageValueForPercentile( - firstBM?.tpot, - lastBM?.tpot, + const itlAvg = getAverageValueForPercentile( + firstBM?.itl, + lastBM?.itl, defaultPercentile ); const timePerRequestAvg = getAverageValueForPercentile( @@ -66,13 +66,13 @@ const setDefaultSLOs = ( currentRequestRate: firstBM?.requestsPerSecond, current: { ttft: formatNumber(ttftAvg, 0), - tpot: formatNumber(tpotAvg, 0), + itl: formatNumber(itlAvg, 0), timePerRequest: formatNumber(timePerRequestAvg, 0), throughput: formatNumber(throughputAvg, 0), }, tasksDefaults: { ttft: formatNumber(ttftAvg, 0), - tpot: formatNumber(tpotAvg, 0), + itl: formatNumber(itlAvg, 0), timePerRequest: formatNumber(timePerRequestAvg, 0), throughput: formatNumber(throughputAvg, 0), }, diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts index 602ae17e..2a5f319e 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts @@ -20,7 +20,7 @@ interface Percentile { export interface BenchmarkMetrics { ttft: Statistics; - tpot: Statistics; + itl: Statistics; timePerRequest: Statistics; throughput: Statistics; } diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts index 53d54f40..9aa5fd81 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts @@ -21,13 +21,13 @@ export const selectMetricsSummaryLineData = createSelector( const lineData: { [K in keyof BenchmarkMetrics]: Point[] } = { ttft: [], - tpot: [], + itl: [], timePerRequest: [], throughput: [], }; const metrics: (keyof BenchmarkMetrics)[] = [ 'ttft', - 'tpot', + 'itl', 'timePerRequest', 'throughput', ]; @@ -66,7 +66,7 @@ export const selectInterpolatedMetrics = createSelector( }; } = { ttft: getDefaultMetricValues(), - tpot: getDefaultMetricValues(), + itl: getDefaultMetricValues(), timePerRequest: getDefaultMetricValues(), throughput: getDefaultMetricValues(), mean: getDefaultMetricValues(), @@ -81,7 +81,7 @@ export const selectInterpolatedMetrics = createSelector( const { enforcedPercentile, currentRequestRate } = sloState; const metrics: (keyof BenchmarkMetrics)[] = [ 'ttft', - 'tpot', + 'itl', 'timePerRequest', 'throughput', ]; @@ -137,13 +137,13 @@ export const selectMetricsDetailsLineData = createSelector( [K in keyof BenchmarkMetrics]: { data: Point[]; id: string; solid?: boolean }[]; } = { ttft: [], - tpot: [], + itl: [], timePerRequest: [], throughput: [], }; const props: (keyof BenchmarkMetrics)[] = [ 'ttft', - 'tpot', + 'itl', 'timePerRequest', 'throughput', ]; diff --git a/src/ui/lib/store/slices/metrics/metrics.constants.ts b/src/ui/lib/store/slices/metrics/metrics.constants.ts index a9ae8414..d61efac1 100644 --- a/src/ui/lib/store/slices/metrics/metrics.constants.ts +++ b/src/ui/lib/store/slices/metrics/metrics.constants.ts @@ -5,6 +5,6 @@ export const initialState: MetricsState = { currentRequestRate: 0, timePerRequest: { valuesByRps: {} }, ttft: { valuesByRps: {} }, - tpot: { valuesByRps: {} }, + itl: { valuesByRps: {} }, throughput: { valuesByRps: {} }, }; diff --git a/src/ui/lib/store/slices/metrics/metrics.interfaces.ts b/src/ui/lib/store/slices/metrics/metrics.interfaces.ts index b38dc98b..bd56018b 100644 --- a/src/ui/lib/store/slices/metrics/metrics.interfaces.ts +++ b/src/ui/lib/store/slices/metrics/metrics.interfaces.ts @@ -4,7 +4,7 @@ export interface MetricsState { currentRequestRate: number; timePerRequest: SingleMetricsState; ttft: SingleMetricsState; - tpot: SingleMetricsState; + itl: SingleMetricsState; throughput: SingleMetricsState; } diff --git a/src/ui/lib/store/slices/slo/slo.constants.ts b/src/ui/lib/store/slices/slo/slo.constants.ts index f58ccc05..d491b712 100644 --- a/src/ui/lib/store/slices/slo/slo.constants.ts +++ b/src/ui/lib/store/slices/slo/slo.constants.ts @@ -10,13 +10,13 @@ export const initialState: SloState = { current: { timePerRequest: 0, ttft: 0, - tpot: 0, + itl: 0, throughput: 0, }, tasksDefaults: { timePerRequest: 0, ttft: 0, - tpot: 0, + itl: 0, throughput: 0, }, }; diff --git a/src/ui/lib/store/slices/slo/slo.interfaces.ts b/src/ui/lib/store/slices/slo/slo.interfaces.ts index 0d59baa2..ecbc2f71 100644 --- a/src/ui/lib/store/slices/slo/slo.interfaces.ts +++ b/src/ui/lib/store/slices/slo/slo.interfaces.ts @@ -6,13 +6,13 @@ export interface SloState { current: { timePerRequest: number; ttft: number; - tpot: number; + itl: number; throughput: number; }; tasksDefaults: { timePerRequest: number; ttft: number; - tpot: number; + itl: number; throughput: number; }; } diff --git a/tests/integration/scheduler/test_scheduler.py b/tests/integration/scheduler/test_scheduler.py index 51abf59b..106f320f 100644 --- a/tests/integration/scheduler/test_scheduler.py +++ b/tests/integration/scheduler/test_scheduler.py @@ -168,6 +168,7 @@ def _request_indices(): received_updates.keys(), received_updates.values(), received_responses, + strict=False, ): assert req == f"req_{index}" assert resp in (f"response_for_{req}", f"mock_error_for_{req}") diff --git a/tests/ui/unit/mocks/mockBenchmarks.ts b/tests/ui/unit/mocks/mockBenchmarks.ts index 884e8b89..e8947508 100644 --- a/tests/ui/unit/mocks/mockBenchmarks.ts +++ b/tests/ui/unit/mocks/mockBenchmarks.ts @@ -1,7 +1,7 @@ export const mockBenchmarks = [ { requestsPerSecond: 0.6668550387660497, - tpot: { + itl: { total: 80, mean: 23.00635663936911, median: 22.959455611213805, @@ -132,7 +132,7 @@ export const mockBenchmarks = [ }, { requestsPerSecond: 28.075330129628725, - tpot: { + itl: { total: 3416, mean: 126.08707076148656, median: 125.30853256346687, diff --git a/tests/unit/backends/test_backend.py b/tests/unit/backends/test_backend.py index ebd0da87..bf3129df 100644 --- a/tests/unit/backends/test_backend.py +++ b/tests/unit/backends/test_backend.py @@ -4,32 +4,20 @@ from __future__ import annotations -import asyncio from collections.abc import AsyncIterator -from functools import wraps from typing import Any from unittest.mock import Mock, patch import pytest from guidellm.backends.backend import Backend, BackendType -from guidellm.backends.objects import ( +from guidellm.scheduler import BackendInterface, ScheduledRequestInfo +from guidellm.schemas.response import ( GenerationRequest, GenerationRequestTimings, ) -from guidellm.scheduler import BackendInterface, ScheduledRequestInfo from guidellm.utils import RegistryMixin - - -def async_timeout(delay): - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout def test_backend_type(): diff --git a/tests/unit/backends/test_objects.py b/tests/unit/backends/test_objects.py index bf903733..600592bc 100644 --- a/tests/unit/backends/test_objects.py +++ b/tests/unit/backends/test_objects.py @@ -9,12 +9,12 @@ import pytest from pydantic import ValidationError -from guidellm.backends.objects import ( +from guidellm.scheduler import MeasuredRequestTimings +from guidellm.schemas.response import ( GenerationRequest, GenerationRequestTimings, GenerationResponse, ) -from guidellm.scheduler import MeasuredRequestTimings from guidellm.utils import StandardBaseModel diff --git a/tests/unit/backends/test_openai_backend.py b/tests/unit/backends/test_openai_backend.py index 2180b501..fefd7a26 100644 --- a/tests/unit/backends/test_openai_backend.py +++ b/tests/unit/backends/test_openai_backend.py @@ -4,9 +4,7 @@ from __future__ import annotations -import asyncio import base64 -from functools import wraps from pathlib import Path from unittest.mock import AsyncMock, Mock, patch @@ -15,24 +13,14 @@ from PIL import Image from guidellm.backends.backend import Backend -from guidellm.backends.objects import ( +from guidellm.backends.openai import OpenAIHTTPBackend +from guidellm.schemas import ( GenerationRequest, - GenerationRequestTimings, GenerationResponse, + RequestInfo, + RequestTimings, ) -from guidellm.backends.openai import OpenAIHTTPBackend, UsageStats -from guidellm.scheduler import ScheduledRequestInfo - - -def async_timeout(delay): - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout def test_usage_stats(): @@ -230,7 +218,6 @@ def test_header_building(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_info(self): """Test info method.""" backend = OpenAIHTTPBackend( @@ -250,7 +237,6 @@ async def test_info(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_process_startup(self): """Test process startup.""" backend = OpenAIHTTPBackend(target="http://test") @@ -267,7 +253,6 @@ async def test_process_startup(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_process_startup_already_started(self): """Test process startup when already started.""" backend = OpenAIHTTPBackend(target="http://test") @@ -279,7 +264,6 @@ async def test_process_startup_already_started(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_process_shutdown(self): """Test process shutdown.""" backend = OpenAIHTTPBackend(target="http://test") @@ -296,7 +280,6 @@ async def test_process_shutdown(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_process_shutdown_not_started(self): """Test process shutdown when not started.""" backend = OpenAIHTTPBackend(target="http://test") @@ -307,7 +290,6 @@ async def test_process_shutdown_not_started(self): @pytest.mark.sanity @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_check_in_process(self): """Test _check_in_process method.""" backend = OpenAIHTTPBackend(target="http://test") @@ -325,7 +307,6 @@ async def test_check_in_process(self): @pytest.mark.sanity @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_available_models(self): """Test available_models method.""" backend = OpenAIHTTPBackend(target="http://test") @@ -346,7 +327,6 @@ async def test_available_models(self): @pytest.mark.sanity @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_default_model(self): """Test default_model method.""" # Test when model is already set @@ -370,7 +350,6 @@ async def test_default_model(self): @pytest.mark.regression @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(10.0) async def test_validate_with_model(self): """Test validate method when model is set.""" backend = OpenAIHTTPBackend(target="http://test", model="test-model") @@ -634,13 +613,13 @@ async def test_resolve_not_implemented_history(self): await backend.process_startup() request = GenerationRequest(content="test") - request_info = ScheduledRequestInfo( + request_info = RequestInfo( request_id="test-id", status="pending", scheduler_node_id=1, scheduler_process_id=1, scheduler_start_time=123.0, - request_timings=GenerationRequestTimings(), + request_timings=RequestTimings(), ) history = [(request, GenerationResponse(request_id="test", request_args={}))] @@ -662,13 +641,13 @@ async def test_resolve_text_completions(self): params={"temperature": 0.7}, constraints={"output_tokens": 100}, ) - request_info = ScheduledRequestInfo( + request_info = RequestInfo( request_id="test-id", status="pending", scheduler_node_id=1, scheduler_process_id=1, scheduler_start_time=123.0, - request_timings=GenerationRequestTimings(), + request_timings=RequestTimings(), ) # Mock text_completions method @@ -703,13 +682,13 @@ async def test_resolve_chat_completions(self): request_type="chat_completions", params={"temperature": 0.5}, ) - request_info = ScheduledRequestInfo( + request_info = RequestInfo( request_id="test-id", status="pending", scheduler_node_id=1, scheduler_process_id=1, scheduler_start_time=123.0, - request_timings=GenerationRequestTimings(), + request_timings=RequestTimings(), ) # Mock chat_completions method @@ -1144,13 +1123,13 @@ async def test_resolve_timing_edge_cases(self): request_type="text_completions", constraints={"output_tokens": 50}, ) - request_info = ScheduledRequestInfo( + request_info = RequestInfo( request_id="test-id", status="pending", scheduler_node_id=1, scheduler_process_id=1, scheduler_start_time=123.0, - request_timings=GenerationRequestTimings(), + request_timings=RequestTimings(), ) # Mock text_completions to test timing edge cases diff --git a/tests/unit/benchmark/test_output.py b/tests/unit/benchmark/test_output.py index 6763d978..6310da88 100644 --- a/tests/unit/benchmark/test_output.py +++ b/tests/unit/benchmark/test_output.py @@ -10,7 +10,10 @@ from guidellm.benchmark import ( GenerativeBenchmarksReport, ) -from guidellm.benchmark.output import GenerativeBenchmarkerConsole, GenerativeBenchmarkerCSV +from guidellm.benchmark.output import ( + GenerativeBenchmarkerConsole, + GenerativeBenchmarkerCSV, +) from tests.unit.mock_benchmark import mock_generative_benchmark @@ -80,6 +83,7 @@ def test_file_yaml(): mock_path.unlink() + @pytest.mark.asyncio async def test_file_csv(): mock_benchmark = mock_generative_benchmark() @@ -89,7 +93,7 @@ async def test_file_csv(): csv_benchmarker = GenerativeBenchmarkerCSV(output_path=mock_path) await csv_benchmarker.finalize(report) - with mock_path.open("r") as file: + with mock_path.open("r") as file: # noqa: ASYNC230 # This is a test. reader = csv.reader(file) headers = next(reader) rows = list(reader) @@ -105,7 +109,8 @@ def test_console_benchmarks_profile_str(): console = GenerativeBenchmarkerConsole() mock_benchmark = mock_generative_benchmark() assert ( - console._get_profile_str(mock_benchmark) == "type=synchronous, strategies=['synchronous']" + console._get_profile_str(mock_benchmark) + == "type=synchronous, strategies=['synchronous']" ) diff --git a/tests/unit/dataset/__init__.py b/tests/unit/data/__init__.py similarity index 100% rename from tests/unit/dataset/__init__.py rename to tests/unit/data/__init__.py diff --git a/tests/unit/data/deserializers/__init__.py b/tests/unit/data/deserializers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/data/deserializers/test_synthetic.py b/tests/unit/data/deserializers/test_synthetic.py new file mode 100644 index 00000000..de95227a --- /dev/null +++ b/tests/unit/data/deserializers/test_synthetic.py @@ -0,0 +1,587 @@ +""" +Unit tests for guidellm.data.deserializers.synthetic module. +""" + +import json +import tempfile +from pathlib import Path +from unittest.mock import Mock + +import pytest +import yaml +from datasets import IterableDataset + +from guidellm.data.deserializers.deserializer import DataNotSupportedError +from guidellm.data.deserializers.synthetic import ( + SyntheticTextDatasetConfig, + SyntheticTextDatasetDeserializer, + SyntheticTextGenerator, + SyntheticTextPrefixBucketConfig, +) + + +class TestPrefixBucketConfig: + """Test cases for PrefixBucketConfig class. + + ### WRITTEN BY AI ### + """ + + @pytest.mark.smoke + def test_creation_with_valid_params(self): + """Test creating PrefixBucketConfig with valid parameters. + + ### WRITTEN BY AI ### + """ + config = SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=5 + ) + + assert config.bucket_weight == 100 + assert config.prefix_count == 1 + assert config.prefix_tokens == 5 + + @pytest.mark.sanity + def test_creation_with_negative_values(self): + """Test creating PrefixBucketConfig with negative values raises ValueError. + + ### WRITTEN BY AI ### + """ + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=-10, prefix_count=1, prefix_tokens=5 + ) + + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=-1, prefix_tokens=5 + ) + + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=-5 + ) + + @pytest.mark.regression + def test_prefix_bucket_zero_weight_error(self): + """Test that zero total weight raises an error. + + ### WRITTEN BY AI ### + """ + # Test validation error for creating PrefixBucketConfig with weight=0 + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=0, prefix_count=1, prefix_tokens=2 + ) + + @pytest.mark.sanity + def test_prefix_bucket_config_validation(self): + """Test PrefixBucketConfig validation. + + ### WRITTEN BY AI ### + """ + # Test valid config + valid_config = SyntheticTextPrefixBucketConfig( + bucket_weight=50, prefix_count=2, prefix_tokens=3 + ) + assert valid_config.bucket_weight == 50 + assert valid_config.prefix_count == 2 + assert valid_config.prefix_tokens == 3 + + # Test invalid bucket_weight + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=0, prefix_count=1, prefix_tokens=2 + ) + + # Test invalid prefix_count + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=0, prefix_tokens=2 + ) + + # Test invalid prefix_tokens + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=-1 + ) + + +class TestSyntheticDatasetConfig: + """Test cases for SyntheticDatasetConfig class. + + ### WRITTEN BY AI ### + """ + + @pytest.mark.smoke + def test_config_creation_with_all_params(self): + """Test creating config with all parameters specified. + + ### WRITTEN BY AI ### + """ + prefix_bucket = SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=5 + ) + + config = SyntheticTextDatasetConfig( + prefix_buckets=[prefix_bucket], + prompt_tokens=100, + prompt_tokens_stdev=10, + prompt_tokens_min=50, + prompt_tokens_max=150, + output_tokens=30, + output_tokens_stdev=5, + output_tokens_min=20, + output_tokens_max=40, + source="custom_text.txt", + ) + + assert config.prefix_buckets[0].prefix_tokens == 5 # type: ignore [index] + assert config.prompt_tokens == 100 + assert config.prompt_tokens_stdev == 10 + assert config.prompt_tokens_min == 50 + assert config.prompt_tokens_max == 150 + assert config.output_tokens == 30 + assert config.output_tokens_stdev == 5 + assert config.output_tokens_min == 20 + assert config.output_tokens_max == 40 + assert config.source == "custom_text.txt" + + @pytest.mark.regression + def test_parse_json_string(self): + """Test parsing JSON string configuration. + + ### WRITTEN BY AI ### + """ + json_str = json.dumps( + { + "prompt_tokens": 75, + "output_tokens": 25, + "source": "test.txt", + "prefix_buckets": [ + {"bucket_weight": 100, "prefix_count": 1, "prefix_tokens": 10} + ], + } + ) + + config = SyntheticTextDatasetConfig.model_validate_json(json_str) + + assert config.prompt_tokens == 75 + assert config.output_tokens == 25 + assert config.source == "test.txt" + assert config.prefix_buckets[0].prefix_tokens == 10 # type: ignore [index] + + @pytest.mark.sanity + def test_validation_positive_values(self): + """Test that negative or zero values are rejected. + + ### WRITTEN BY AI ### + """ + with pytest.raises(ValueError): + SyntheticTextDatasetConfig(prompt_tokens=0, output_tokens=20) + + with pytest.raises(ValueError): + SyntheticTextDatasetConfig(prompt_tokens=20, output_tokens=0) + + # Test negative prefix tokens via PrefixBucketConfig validation + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig(prefix_tokens=-1) + + @pytest.mark.regression + def test_validation_optional_positive_values(self): + """Test that optional parameters reject negative values. + + ### WRITTEN BY AI ### + """ + with pytest.raises(ValueError): + SyntheticTextDatasetConfig( + prompt_tokens=20, output_tokens=10, prompt_tokens_stdev=-1 + ) + + with pytest.raises(ValueError): + SyntheticTextDatasetConfig( + prompt_tokens=20, output_tokens=10, prompt_tokens_min=-1 + ) + + with pytest.raises(ValueError): + SyntheticTextDatasetConfig( + prompt_tokens=20, output_tokens=10, output_tokens_max=0 + ) + + +class TestSyntheticTextGenerator: + """Test cases for SyntheticTextGenerator class. + + ### WRITTEN BY AI ### + """ + + @pytest.fixture + def mock_tokenizer(self): + """Fixture to provide a mocked tokenizer. + + ### WRITTEN BY AI ### + """ + tokenizer = Mock() + tokenizer.encode.side_effect = lambda text: list(range(len(text.split()))) + tokenizer.decode.side_effect = ( + lambda tokens, skip_special_tokens=False: " ".join( + f"token_{t}" for t in tokens[:5] + ) + ) + return tokenizer + + @pytest.fixture + def simple_config(self): + """Fixture for simple configuration. + + ### WRITTEN BY AI ### + """ + return SyntheticTextDatasetConfig( + prompt_tokens=15, + output_tokens=10, + source="The quick brown fox jumps over the lazy dog.", + ) + + @pytest.fixture + def config_with_prefix(self): + """Fixture for configuration with prefix tokens. + + ### WRITTEN BY AI ### + """ + prefix_bucket = SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=3 + ) + + return SyntheticTextDatasetConfig( + prefix_buckets=[prefix_bucket], + prompt_tokens=15, + output_tokens=10, + source="The quick brown fox jumps over the lazy dog.", + ) + + @pytest.mark.smoke + def test_generator_initialization(self, simple_config, mock_tokenizer): + """Test generator initialization. + + ### WRITTEN BY AI ### + """ + generator = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + + assert generator.config == simple_config + assert generator.processor == mock_tokenizer + assert generator.random_seed == 42 + + @pytest.mark.smoke + def test_basic_iteration(self, simple_config, mock_tokenizer): + """Test basic iteration functionality. + + ### WRITTEN BY AI ### + """ + generator = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + + items = [] + for i, item in enumerate(generator): + items.append(item) + if i >= 4: # Only get 5 items + break + + # Verify we get the expected number of items + assert len(items) == 5 + + # Verify each item has the required keys + for item in items: + assert "prefix" in item + assert "prompt" in item + assert "prompt_tokens_count" in item + assert "output_tokens_count" in item + assert isinstance(item["prefix"], str) + assert isinstance(item["prompt"], str) + assert isinstance(item["prompt_tokens_count"], int) + assert isinstance(item["output_tokens_count"], int) + + @pytest.mark.sanity + def test_create_prompt_method(self, simple_config, mock_tokenizer): + """Test _create_prompt method. + + ### WRITTEN BY AI ### + """ + from faker import Faker + + generator = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + faker = Faker() + faker.seed_instance(42) + + # Test normal case + result = generator._create_prompt(5, faker, "unique_prefix ") + assert isinstance(result, str) + # The result should be the decoded tokens (token_0 token_1 etc.) due to our mock + assert "token_" in result + + # Test zero tokens + result = generator._create_prompt(0, faker) + assert result == "" + + @pytest.mark.regression + def test_prefix_tokens_integration(self, config_with_prefix, mock_tokenizer): + """Test integration with prefix tokens. + + ### WRITTEN BY AI ### + """ + generator = SyntheticTextGenerator( + config_with_prefix, mock_tokenizer, random_seed=42 + ) + + items = [] + for i, item in enumerate(generator): + items.append(item) + if i >= 2: # Only get 3 items + break + + # Verify prefix is present in items + for item in items: + assert isinstance(item["prefix"], str) + + @pytest.mark.regression + def test_random_seeding_consistency(self, simple_config, mock_tokenizer): + """Test that same seed produces consistent results. + + ### WRITTEN BY AI ### + """ + # Create two generators with same seed + generator1 = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + generator2 = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + + items1 = [] + items2 = [] + for i, (item1, item2) in enumerate(zip(generator1, generator2, strict=False)): + items1.append(item1) + items2.append(item2) + if i >= 2: # Only get 3 items + break + + # With same seed and deterministic mocks, results should be identical + assert len(items1) == len(items2) + for item1, item2 in zip(items1, items2, strict=False): + assert item1["prompt_tokens_count"] == item2["prompt_tokens_count"] + assert item1["output_tokens_count"] == item2["output_tokens_count"] + + +class TestSyntheticDatasetDeserializer: + """Test cases for SyntheticDatasetDeserializer class. + + ### WRITTEN BY AI ### + """ + + @pytest.fixture + def mock_tokenizer(self): + """Fixture to provide a mocked tokenizer. + + ### WRITTEN BY AI ### + """ + tokenizer = Mock() + tokenizer.encode.side_effect = lambda text: list(range(len(text.split()))) + tokenizer.decode.side_effect = ( + lambda tokens, skip_special_tokens=False: " ".join( + f"token_{t}" for t in tokens[:5] + ) + ) + return tokenizer + + @pytest.mark.sanity + def test_load_config_file_yaml(self): + """Test loading YAML config file. + + ### WRITTEN BY AI ### + """ + config_data = { + "prompt_tokens": 60, + "output_tokens": 15, + "source": "yaml_test.txt", + "prefix_buckets": [ + {"bucket_weight": 100, "prefix_count": 1, "prefix_tokens": 3} + ], + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config_data, f) + yaml_path = f.name + + try: + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_file(yaml_path) + + assert config.prompt_tokens == 60 + assert config.output_tokens == 15 + assert config.source == "yaml_test.txt" + assert config.prefix_buckets[0].prefix_tokens == 3 # type: ignore [index] + finally: + Path(yaml_path).unlink() + + @pytest.mark.sanity + def test_load_config_file_config_extension(self): + """Test loading .config file. + + ### WRITTEN BY AI ### + """ + config_data = { + "prompt_tokens": 90, + "output_tokens": 35, + "prefix_buckets": [ + {"bucket_weight": 100, "prefix_count": 1, "prefix_tokens": 2} + ], + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".config", delete=False) as f: + yaml.dump(config_data, f) + config_path = f.name + + try: + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_file(config_path) + + assert config.prompt_tokens == 90 + assert config.output_tokens == 35 + assert config.prefix_buckets[0].prefix_tokens == 2 # type: ignore [index] + finally: + Path(config_path).unlink() + + @pytest.mark.smoke + def test_load_config_str_json(self): + """Test loading JSON string config. + + ### WRITTEN BY AI ### + """ + json_str = '{"prompt_tokens": 50, "output_tokens": 25}' + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_str(json_str) + + assert config.prompt_tokens == 50 + assert config.output_tokens == 25 + + @pytest.mark.smoke + def test_load_config_str_key_value(self): + """Test loading key-value string config. + + ### WRITTEN BY AI ### + """ + kv_str = "prompt_tokens=50,output_tokens=25" + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_str(kv_str) + + assert config.prompt_tokens == 50 + assert config.output_tokens == 25 + + @pytest.mark.sanity + def test_load_config_str_invalid_format(self): + """Test loading invalid format raises DataNotSupportedError. + + ### WRITTEN BY AI ### + """ + deserializer = SyntheticTextDatasetDeserializer() + with pytest.raises(DataNotSupportedError, match="Unsupported string data"): + deserializer._load_config_str("invalid_format_string") + + @pytest.mark.regression + def test_load_config_file_non_existent(self): + """Test loading non-existent file returns None. + + ### WRITTEN BY AI ### + """ + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_file("/non/existent/path.config") + assert config is None + + @pytest.mark.regression + def test_load_config_str_non_string(self): + """Test loading non-string returns None. + + ### WRITTEN BY AI ### + """ + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_str(123) + assert config is None + + @pytest.mark.smoke + def test_call_with_config_object(self, mock_tokenizer): + """Test calling deserializer with SyntheticTextDatasetConfig. + + ### WRITTEN BY AI ### + """ + config = SyntheticTextDatasetConfig(prompt_tokens=50, output_tokens=25) + deserializer = SyntheticTextDatasetDeserializer() + + result = deserializer( + data=config, + data_kwargs={}, + processor_factory=lambda: mock_tokenizer, + random_seed=42, + ) + + assert isinstance(result, IterableDataset) + + @pytest.mark.regression + def test_call_with_unsupported_data(self, mock_tokenizer): + """Test calling deserializer with unsupported data raises error. + + ### WRITTEN BY AI ### + """ + deserializer = SyntheticTextDatasetDeserializer() + + with pytest.raises(DataNotSupportedError, match="Unsupported data"): + deserializer( + data=123, + data_kwargs={}, + processor_factory=lambda: mock_tokenizer, + random_seed=42, + ) + + @pytest.mark.regression + def test_call_with_json_string(self, mock_tokenizer): + """Test calling deserializer with JSON string. + + ### WRITTEN BY AI ### + """ + json_str = '{"prompt_tokens": 50, "output_tokens": 25}' + deserializer = SyntheticTextDatasetDeserializer() + + result = deserializer( + data=json_str, + data_kwargs={}, + processor_factory=lambda: mock_tokenizer, + random_seed=42, + ) + + assert isinstance(result, IterableDataset) + + @pytest.mark.regression + def test_call_with_config_file(self, mock_tokenizer): + """Test calling deserializer with config file. + + ### WRITTEN BY AI ### + """ + config_data = {"prompt_tokens": 65, "output_tokens": 45} + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config_data, f) + config_path = f.name + + try: + deserializer = SyntheticTextDatasetDeserializer() + result = deserializer( + data=config_path, + data_kwargs={}, + processor_factory=lambda: mock_tokenizer, + random_seed=42, + ) + assert isinstance(result, IterableDataset) + finally: + Path(config_path).unlink() diff --git a/tests/unit/dataset/test_synthetic.py b/tests/unit/dataset/test_synthetic.py deleted file mode 100644 index e3110fa3..00000000 --- a/tests/unit/dataset/test_synthetic.py +++ /dev/null @@ -1,873 +0,0 @@ -""" -Unit tests for guidellm.dataset.synthetic module. -""" - -import json -import tempfile -from pathlib import Path -from unittest.mock import Mock, patch - -import pytest -import yaml - -from guidellm.dataset.synthetic import ( - SyntheticDatasetConfig, - SyntheticDatasetCreator, - SyntheticTextItemsGenerator, -) - - -class TestSyntheticDatasetConfig: - """Test cases for SyntheticDatasetConfig class. - - ### WRITTEN BY AI ### - """ - - @pytest.mark.smoke - def test_config_creation_with_all_params(self): - """Test creating config with all parameters specified. - - ### WRITTEN BY AI ### - """ - config = SyntheticDatasetConfig( - prefix_tokens=5, - prompt_tokens=100, - prompt_tokens_stdev=10, - prompt_tokens_min=50, - prompt_tokens_max=150, - output_tokens=30, - output_tokens_stdev=5, - output_tokens_min=20, - output_tokens_max=40, - samples=500, - source="custom_text.txt", - ) - - assert config.prefix_tokens == 5 - assert config.prompt_tokens == 100 - assert config.prompt_tokens_stdev == 10 - assert config.prompt_tokens_min == 50 - assert config.prompt_tokens_max == 150 - assert config.output_tokens == 30 - assert config.output_tokens_stdev == 5 - assert config.output_tokens_min == 20 - assert config.output_tokens_max == 40 - assert config.samples == 500 - assert config.source == "custom_text.txt" - - @pytest.mark.regression - def test_parse_json_string(self): - """Test parsing JSON string configuration. - - ### WRITTEN BY AI ### - """ - json_str = json.dumps( - { - "prompt_tokens": 75, - "output_tokens": 25, - "samples": 200, - "source": "test.txt", - "prefix_tokens": 10, - } - ) - - config = SyntheticDatasetConfig.parse_str(json_str) - - assert config.prompt_tokens == 75 - assert config.output_tokens == 25 - assert config.samples == 200 - assert config.source == "test.txt" - assert config.prefix_tokens == 10 - - @pytest.mark.regression - def test_parse_key_value_pairs(self): - """Test parsing key-value pairs configuration. - - ### WRITTEN BY AI ### - """ - kv_str = "prompt_tokens=80,output_tokens=30,samples=300,source=data.txt,prefix_tokens=5" # noqa: E501 - - config = SyntheticDatasetConfig.parse_str(kv_str) - - assert config.prompt_tokens == 80 - assert config.output_tokens == 30 - assert config.samples == 300 - assert config.source == "data.txt" - assert config.prefix_tokens == 5 - - @pytest.mark.sanity - def test_parse_yaml_file(self): - """Test parsing YAML file configuration. - - ### WRITTEN BY AI ### - """ - config_data = { - "prompt_tokens": 60, - "output_tokens": 15, - "samples": 100, - "source": "yaml_test.txt", - "prefix_tokens": 3, - } - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(config_data, f) - yaml_path = f.name - - try: - config = SyntheticDatasetConfig.parse_str(yaml_path) - - assert config.prompt_tokens == 60 - assert config.output_tokens == 15 - assert config.samples == 100 - assert config.source == "yaml_test.txt" - assert config.prefix_tokens == 3 - finally: - Path(yaml_path).unlink() - - @pytest.mark.sanity - def test_parse_config_file(self): - """Test parsing .config file. - - ### WRITTEN BY AI ### - """ - config_data = { - "prompt_tokens": 90, - "output_tokens": 35, - "samples": 150, - "prefix_tokens": 2, - } - - with tempfile.NamedTemporaryFile(mode="w", suffix=".config", delete=False) as f: - yaml.dump(config_data, f) - config_path = f.name - - try: - config = SyntheticDatasetConfig.parse_str(config_path) - - assert config.prompt_tokens == 90 - assert config.output_tokens == 35 - assert config.samples == 150 - assert config.prefix_tokens == 2 - finally: - Path(config_path).unlink() - - @pytest.mark.regression - def test_parse_path_object(self): - """Test parsing with Path object. - - ### WRITTEN BY AI ### - """ - config_data = {"prompt_tokens": 45, "output_tokens": 25} - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(config_data, f) - yaml_path = Path(f.name) - - try: - config = SyntheticDatasetConfig.parse_str(yaml_path) - assert config.prompt_tokens == 45 - assert config.output_tokens == 25 - finally: - yaml_path.unlink() - - @pytest.mark.sanity - def test_parse_invalid_format(self): - """Test parsing invalid format raises ValueError. - - ### WRITTEN BY AI ### - """ - with pytest.raises(ValueError, match="Unsupported data format"): - SyntheticDatasetConfig.parse_str("invalid_format_string") - - @pytest.mark.sanity - def test_validation_positive_values(self): - """Test that negative or zero values are rejected. - - ### WRITTEN BY AI ### - """ - with pytest.raises(ValueError): - SyntheticDatasetConfig(prompt_tokens=0, output_tokens=20) - - with pytest.raises(ValueError): - SyntheticDatasetConfig(prompt_tokens=20, output_tokens=0) - - with pytest.raises(ValueError): - SyntheticDatasetConfig(prompt_tokens=20, output_tokens=10, samples=0) - - with pytest.raises(ValueError): - SyntheticDatasetConfig(prompt_tokens=20, output_tokens=10, prefix_tokens=-1) - - @pytest.mark.regression - def test_validation_optional_positive_values(self): - """Test that optional parameters reject negative values. - - ### WRITTEN BY AI ### - """ - with pytest.raises(ValueError): - SyntheticDatasetConfig( - prompt_tokens=20, output_tokens=10, prompt_tokens_stdev=-1 - ) - - with pytest.raises(ValueError): - SyntheticDatasetConfig( - prompt_tokens=20, output_tokens=10, prompt_tokens_min=-1 - ) - - with pytest.raises(ValueError): - SyntheticDatasetConfig( - prompt_tokens=20, output_tokens=10, output_tokens_max=0 - ) - - @pytest.mark.regression - def test_parse_json_method_directly(self): - """Test parse_json static method directly. - - ### WRITTEN BY AI ### - """ - json_data = {"prompt_tokens": 100, "output_tokens": 50} - json_str = json.dumps(json_data) - - config = SyntheticDatasetConfig.parse_json(json_str) - - assert config.prompt_tokens == 100 - assert config.output_tokens == 50 - - @pytest.mark.regression - def test_parse_key_value_pairs_method_directly(self): - """Test parse_key_value_pairs static method directly. - - ### WRITTEN BY AI ### - """ - kv_str = "prompt_tokens=75,output_tokens=35" - - config = SyntheticDatasetConfig.parse_key_value_pairs(kv_str) - - assert config.prompt_tokens == 75 - assert config.output_tokens == 35 - - @pytest.mark.regression - def test_parse_config_file_method_directly(self): - """Test parse_config_file static method directly. - - ### WRITTEN BY AI ### - """ - config_data = {"prompt_tokens": 65, "output_tokens": 45} - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(config_data, f) - config_path = f.name - - try: - config = SyntheticDatasetConfig.parse_config_file(config_path) - assert config.prompt_tokens == 65 - assert config.output_tokens == 45 - finally: - Path(config_path).unlink() - - -class TestSyntheticTextItemsGenerator: - """Test cases for SyntheticTextItemsGenerator class. - - ### WRITTEN BY AI ### - """ - - @pytest.fixture - def mock_tokenizer(self): - """Fixture to provide a mocked tokenizer. - - ### WRITTEN BY AI ### - """ - tokenizer = Mock() - tokenizer.get_vocab.return_value = {f"token_{i}": i for i in range(1000)} - tokenizer.encode.side_effect = lambda text: [1, 2, 3] * (len(text) // 10 + 1) - tokenizer.decode.side_effect = ( - lambda tokens, skip_special_tokens=False: " ".join( - f"token_{t}" for t in tokens[:5] - ) - ) - return tokenizer - - @pytest.fixture - def simple_config(self): - """Fixture for simple configuration. - - ### WRITTEN BY AI ### - """ - return SyntheticDatasetConfig( - prompt_tokens=15, - output_tokens=10, - samples=5, - source="The quick brown fox jumps over the lazy dog.", - ) - - @pytest.fixture - def config_with_prefix(self): - """Fixture for configuration with prefix tokens. - - ### WRITTEN BY AI ### - """ - return SyntheticDatasetConfig( - prefix_tokens=3, - prompt_tokens=15, - output_tokens=10, - samples=5, - source="The quick brown fox jumps over the lazy dog.", - ) - - @pytest.fixture - def complex_config(self): - """Fixture for complex configuration with variance. - - ### WRITTEN BY AI ### - """ - return SyntheticDatasetConfig( - prompt_tokens=20, - prompt_tokens_stdev=5, - prompt_tokens_min=10, - prompt_tokens_max=30, - output_tokens=15, - output_tokens_stdev=3, - output_tokens_min=10, - output_tokens_max=20, - samples=10, - source="The quick brown fox jumps over the lazy dog.", - ) - - @pytest.mark.smoke - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - def test_generator_initialization( - self, mock_text_creator, simple_config, mock_tokenizer - ): - """Test generator initialization. - - ### WRITTEN BY AI ### - """ - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - assert generator.config == simple_config - assert generator.processor == mock_tokenizer - assert generator.random_seed == 42 - mock_text_creator.assert_called_once_with(data=simple_config.source) - - @pytest.mark.smoke - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - @patch("guidellm.dataset.synthetic.IntegerRangeSampler") - def test_basic_iteration( - self, mock_sampler, mock_text_creator, simple_config, mock_tokenizer - ): - """Test basic iteration functionality. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word1", "word2", "word3"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - # Mock IntegerRangeSampler to return iterators - def mock_sampler_side_effect(*args, **kwargs): - mock_instance = Mock() - mock_instance.__iter__ = Mock(return_value=iter([15, 15, 15, 15, 15])) - return mock_instance - - mock_sampler.side_effect = mock_sampler_side_effect - - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - items = list(generator) - - # Verify we get the expected number of items - assert len(items) == simple_config.samples - - # Verify each item has the required keys - for item in items: - assert "prompt" in item - assert "prompt_tokens_count" in item - assert "output_tokens_count" in item - assert isinstance(item["prompt"], str) - assert isinstance(item["prompt_tokens_count"], int) - assert isinstance(item["output_tokens_count"], int) - - @pytest.mark.sanity - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - def test_create_prompt_method( - self, mock_text_creator, simple_config, mock_tokenizer - ): - """Test _create_prompt method. - - ### WRITTEN BY AI ### - """ - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "test text" - mock_text_creator.return_value = mock_text_creator_instance - - mock_tokenizer.encode.return_value = [1, 2, 3] - - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - # Test normal case - result = generator._create_prompt(5, 0, 42) - assert result == [42, 1, 2, 3] - - # Test zero tokens - result = generator._create_prompt(0, 0, 42) - assert result == [] - - # Test without unique prefix - result = generator._create_prompt(3, 0) - assert result == [1, 2, 3] - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - def test_create_prompt_binary_search( - self, mock_text_creator, simple_config, mock_tokenizer - ): - """Test binary search logic in _create_prompt. - - ### WRITTEN BY AI ### - """ - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 1000 - mock_text_creator_instance.create_text.side_effect = lambda start, length: ( - "text " * max(1, length // 4) - ).strip() - mock_text_creator.return_value = mock_text_creator_instance - - # Mock tokenizer to return different lengths based on input - def mock_encode(text): - return [1] * len(text.split()) - - mock_tokenizer.encode.side_effect = mock_encode - - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - # Test that binary search finds appropriate length - result = generator._create_prompt(5, 0, 42) - assert len(result) >= 4 # Should include prefix + some tokens - - @pytest.mark.sanity - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - @patch("guidellm.dataset.synthetic.IntegerRangeSampler") - def test_prefix_tokens_integration( - self, mock_sampler, mock_text_creator, config_with_prefix, mock_tokenizer - ): - """Test integration with prefix tokens. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - mock_sampler_instance = Mock() - mock_sampler_instance.__iter__ = Mock(return_value=iter([15, 15, 15, 15, 15])) - mock_sampler.return_value = mock_sampler_instance - - generator = SyntheticTextItemsGenerator( - config_with_prefix, mock_tokenizer, random_seed=42 - ) - - items = list(generator) - - # Verify prompt_tokens_count includes prefix - for item in items: - assert item["prompt_tokens_count"] == config_with_prefix.prefix_tokens + 15 - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - @patch("guidellm.dataset.synthetic.IntegerRangeSampler") - def test_random_seeding_consistency( - self, mock_sampler, mock_text_creator, simple_config, mock_tokenizer - ): - """Test that same seed produces consistent results. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - # Create consistent mock sampler behavior - call_count = 0 - - def mock_sampler_side_effect(*args, **kwargs): - nonlocal call_count - mock_instance = Mock() - # Return same sequence for both prompt and output tokens - if call_count % 2 == 0: # prompt tokens - mock_instance.__iter__ = Mock(return_value=iter([15, 16, 17, 18, 19])) - else: # output tokens - mock_instance.__iter__ = Mock(return_value=iter([10, 11, 12, 13, 14])) - call_count += 1 - return mock_instance - - mock_sampler.side_effect = mock_sampler_side_effect - - # Create two generators with same seed - generator1 = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - generator2 = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - items1 = list(generator1) - items2 = list(generator2) - - # Results should be identical with same seed - assert len(items1) == len(items2) - for item1, item2 in zip(items1, items2): - assert item1["prompt"] == item2["prompt"] - assert item1["prompt_tokens_count"] == item2["prompt_tokens_count"] - assert item1["output_tokens_count"] == item2["output_tokens_count"] - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - @patch("guidellm.dataset.synthetic.IntegerRangeSampler") - def test_variance_configuration( - self, mock_sampler, mock_text_creator, complex_config, mock_tokenizer - ): - """Test that variance configuration is properly used. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - # Fix tokenizer mock to handle the create_text return properly - mock_tokenizer.encode.side_effect = ( - lambda text: [1, 2, 3] if isinstance(text, str) else [1, 2, 3] - ) - - # Setup mock sampler to track calls - def mock_sampler_side_effect(*args, **kwargs): - mock_instance = Mock() - mock_instance.__iter__ = Mock(return_value=iter([20, 18, 22, 19, 21] * 2)) - return mock_instance - - mock_sampler.side_effect = mock_sampler_side_effect - - generator = SyntheticTextItemsGenerator( - complex_config, mock_tokenizer, random_seed=42 - ) - - # Initialize the generator to trigger sampler creation - generator_iter = iter(generator) - next(generator_iter) - - # Verify that IntegerRangeSampler is called with correct parameters - assert mock_sampler.call_count == 2 - - # Check prompt tokens sampler call - prompt_call = mock_sampler.call_args_list[0] - assert prompt_call[1]["average"] == complex_config.prompt_tokens - assert prompt_call[1]["variance"] == complex_config.prompt_tokens_stdev - assert prompt_call[1]["min_value"] == complex_config.prompt_tokens_min - assert prompt_call[1]["max_value"] == complex_config.prompt_tokens_max - assert prompt_call[1]["random_seed"] == 42 - - # Check output tokens sampler call - output_call = mock_sampler.call_args_list[1] - assert output_call[1]["average"] == complex_config.output_tokens - assert output_call[1]["variance"] == complex_config.output_tokens_stdev - assert output_call[1]["min_value"] == complex_config.output_tokens_min - assert output_call[1]["max_value"] == complex_config.output_tokens_max - assert output_call[1]["random_seed"] == 43 # 42 + 1 - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - def test_unique_prefix_generation( - self, mock_text_creator, simple_config, mock_tokenizer - ): - """Test that unique prefixes are generated for each request. - - ### WRITTEN BY AI ### - """ - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - # Mock the cycle to return predictable values - with patch("guidellm.dataset.synthetic.cycle") as mock_cycle: - mock_cycle.return_value = iter([100, 101, 102, 103, 104]) - - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - # Access the iterator to trigger the cycle creation - generator_iter = iter(generator) - next(generator_iter) - - # Verify cycle was called with vocab values - mock_cycle.assert_called_once() - - -class TestSyntheticDatasetCreator: - """Test cases for SyntheticDatasetCreator class. - - ### WRITTEN BY AI ### - """ - - @pytest.mark.sanity - def test_is_supported_path_config_file(self): - """Test is_supported with config file paths. - - ### WRITTEN BY AI ### - """ - with tempfile.NamedTemporaryFile(suffix=".config", delete=False) as f: - config_path = Path(f.name) - - try: - assert SyntheticDatasetCreator.is_supported(config_path, None) - finally: - config_path.unlink() - - @pytest.mark.sanity - def test_is_supported_path_yaml_file(self): - """Test is_supported with YAML file paths. - - ### WRITTEN BY AI ### - """ - with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as f: - yaml_path = Path(f.name) - - try: - assert SyntheticDatasetCreator.is_supported(yaml_path, None) - finally: - yaml_path.unlink() - - @pytest.mark.smoke - def test_is_supported_json_string(self): - """Test is_supported with JSON string. - - ### WRITTEN BY AI ### - """ - json_str = '{"prompt_tokens": 50, "output_tokens": 25}' - assert SyntheticDatasetCreator.is_supported(json_str, None) - - @pytest.mark.smoke - def test_is_supported_key_value_string(self): - """Test is_supported with key-value string. - - ### WRITTEN BY AI ### - """ - kv_str = "prompt_tokens=50,output_tokens=25" - assert SyntheticDatasetCreator.is_supported(kv_str, None) - - @pytest.mark.sanity - def test_is_supported_config_filename_string(self): - """Test is_supported with config filename string. - - ### WRITTEN BY AI ### - """ - assert SyntheticDatasetCreator.is_supported("config.yaml", None) - assert SyntheticDatasetCreator.is_supported("settings.config", None) - - @pytest.mark.sanity - def test_is_not_supported_regular_string(self): - """Test is_supported returns False for regular strings. - - ### WRITTEN BY AI ### - """ - assert not SyntheticDatasetCreator.is_supported("regular string", None) - assert not SyntheticDatasetCreator.is_supported("single=pair", None) - - @pytest.mark.regression - def test_is_not_supported_non_existent_path(self): - """Test is_supported returns False for non-existent paths. - - ### WRITTEN BY AI ### - """ - non_existent_path = Path("/non/existent/path.config") - assert not SyntheticDatasetCreator.is_supported(non_existent_path, None) - - @pytest.mark.regression - def test_is_not_supported_other_types(self): - """Test is_supported returns False for other data types. - - ### WRITTEN BY AI ### - """ - assert not SyntheticDatasetCreator.is_supported(123, None) - assert not SyntheticDatasetCreator.is_supported(["list"], None) - assert not SyntheticDatasetCreator.is_supported({"dict": "value"}, None) - - @pytest.mark.smoke - @patch("guidellm.dataset.synthetic.check_load_processor") - @patch("guidellm.dataset.synthetic.SyntheticTextItemsGenerator") - @patch("guidellm.dataset.synthetic.Dataset") - def test_handle_create_basic( - self, mock_dataset, mock_generator, mock_check_processor - ): - """Test handle_create basic functionality. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_processor = Mock() - mock_check_processor.return_value = mock_processor - - mock_generator_instance = Mock() - mock_generator_instance.__iter__ = Mock( - return_value=iter( - [ - { - "prompt": "test", - "prompt_tokens_count": 10, - "output_tokens_count": 5, - } - ] - ) - ) - mock_generator.return_value = mock_generator_instance - - mock_dataset_instance = Mock() - mock_dataset.from_list.return_value = mock_dataset_instance - - # Test - data = '{"prompt_tokens": 50, "output_tokens": 25}' - result = SyntheticDatasetCreator.handle_create( - data=data, - data_args=None, - processor="gpt2", - processor_args=None, - random_seed=42, - ) - - # Verify - mock_check_processor.assert_called_once_with( - "gpt2", - None, - error_msg="Processor/tokenizer required for synthetic dataset generation.", - ) - mock_generator.assert_called_once() - mock_dataset.from_list.assert_called_once() - assert result == mock_dataset_instance - - @pytest.mark.sanity - @patch("guidellm.dataset.synthetic.check_load_processor") - def test_handle_create_processor_required(self, mock_check_processor): - """Test handle_create requires processor. - - ### WRITTEN BY AI ### - """ - mock_check_processor.side_effect = ValueError("Processor required") - - data = '{"prompt_tokens": 50, "output_tokens": 25}' - - with pytest.raises(ValueError, match="Processor required"): - SyntheticDatasetCreator.handle_create( - data=data, - data_args=None, - processor=None, - processor_args=None, - random_seed=42, - ) - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.check_load_processor") - @patch("guidellm.dataset.synthetic.SyntheticTextItemsGenerator") - @patch("guidellm.dataset.synthetic.Dataset") - def test_handle_create_with_data_args( - self, mock_dataset, mock_generator, mock_check_processor - ): - """Test handle_create with data_args. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_processor = Mock() - mock_check_processor.return_value = mock_processor - - mock_generator_instance = Mock() - mock_generator_instance.__iter__ = Mock(return_value=iter([])) - mock_generator.return_value = mock_generator_instance - - mock_dataset_instance = Mock() - mock_dataset.from_list.return_value = mock_dataset_instance - - # Test with data_args - data = '{"prompt_tokens": 50, "output_tokens": 25}' - data_args = {"features": "custom_features"} - - SyntheticDatasetCreator.handle_create( - data=data, - data_args=data_args, - processor="gpt2", - processor_args=None, - random_seed=42, - ) - - # Verify data_args are passed to Dataset.from_list - mock_dataset.from_list.assert_called_once_with([], **data_args) - - @pytest.mark.sanity - def test_extract_args_column_mappings_empty(self): - """Test extract_args_column_mappings with empty data_args. - - ### WRITTEN BY AI ### - """ - result = SyntheticDatasetCreator.extract_args_column_mappings(None) - - expected = { - "prompt_column": "prompt", - "prompt_tokens_count_column": "prompt_tokens_count", - "output_tokens_count_column": "output_tokens_count", - } - assert result == expected - - @pytest.mark.regression - def test_extract_args_column_mappings_with_parent_mappings(self): - """Test extract_args_column_mappings rejects column mappings. - - ### WRITTEN BY AI ### - """ - with ( - patch.object( - SyntheticDatasetCreator.__bases__[0], - "extract_args_column_mappings", - return_value={"prompt_column": "custom_prompt"}, - ), - pytest.raises(ValueError, match="Column mappings are not supported"), - ): - SyntheticDatasetCreator.extract_args_column_mappings({"some": "args"}) - - @pytest.mark.regression - def test_extract_args_column_mappings_no_parent_mappings(self): - """Test extract_args_column_mappings with no parent mappings. - - ### WRITTEN BY AI ### - """ - with patch.object( - SyntheticDatasetCreator.__bases__[0], - "extract_args_column_mappings", - return_value={}, - ): - result = SyntheticDatasetCreator.extract_args_column_mappings( - {"some": "args"} - ) - - expected = { - "prompt_column": "prompt", - "prompt_tokens_count_column": "prompt_tokens_count", - "output_tokens_count_column": "output_tokens_count", - } - assert result == expected diff --git a/tests/unit/mock_backend.py b/tests/unit/mock_backend.py index 5ac069a8..3b7237e0 100644 --- a/tests/unit/mock_backend.py +++ b/tests/unit/mock_backend.py @@ -6,7 +6,7 @@ import random import time from collections.abc import AsyncIterator -from typing import Any, Optional +from typing import Any from lorem.text import TextLorem @@ -32,7 +32,7 @@ def __init__( self, target: str = "mock-target", model: str = "mock-model", - iter_delay: Optional[float] = None, + iter_delay: float | None = None, ): """ Initialize mock backend. @@ -53,7 +53,7 @@ def target(self) -> str: return self._target @property - def model(self) -> Optional[str]: + def model(self) -> str | None: """Model name for the mock backend.""" return self._model @@ -87,7 +87,7 @@ async def validate(self) -> None: if not self._in_process: raise RuntimeError("Backend not started up for process") - async def default_model(self) -> Optional[str]: + async def default_model(self) -> str | None: """ Return the default model for the mock backend. """ @@ -97,7 +97,7 @@ async def resolve( self, request: GenerationRequest, request_info: ScheduledRequestInfo, - history: Optional[list[tuple[GenerationRequest, GenerationResponse]]] = None, + history: list[tuple[GenerationRequest, GenerationResponse]] | None = None, ) -> AsyncIterator[tuple[GenerationResponse, ScheduledRequestInfo]]: """ Process a generation request and yield progressive responses. @@ -170,7 +170,7 @@ def _estimate_prompt_tokens(content: str) -> int: return len(str(content).split()) @staticmethod - def _get_tokens(token_count: Optional[int] = None) -> list[str]: + def _get_tokens(token_count: int | None = None) -> list[str]: """ Generate mock tokens for response. """ diff --git a/tests/unit/mock_benchmark.py b/tests/unit/mock_benchmark.py index cdf4375a..9201d621 100644 --- a/tests/unit/mock_benchmark.py +++ b/tests/unit/mock_benchmark.py @@ -1,4 +1,5 @@ """Mock benchmark objects for unit testing.""" + from guidellm.backends import GenerationRequestTimings from guidellm.benchmark import ( BenchmarkSchedulerStats, @@ -6,8 +7,8 @@ GenerativeMetrics, GenerativeRequestStats, ) -from guidellm.benchmark.objects import BenchmarkerDict, SchedulerDict from guidellm.benchmark.profile import SynchronousProfile +from guidellm.benchmark.schemas import BenchmarkerDict, SchedulerDict from guidellm.scheduler import ScheduledRequestInfo, SchedulerState, SynchronousStrategy from guidellm.utils import ( DistributionSummary, diff --git a/tests/unit/mock_server/test_server.py b/tests/unit/mock_server/test_server.py index 008103c3..ba712fb6 100644 --- a/tests/unit/mock_server/test_server.py +++ b/tests/unit/mock_server/test_server.py @@ -162,7 +162,7 @@ async def test_health_endpoint(self, mock_server_instance): assert "status" in data assert data["status"] == "healthy" assert "timestamp" in data - assert isinstance(data["timestamp"], (int, float)) + assert isinstance(data["timestamp"], int | float) @pytest.mark.smoke @pytest.mark.asyncio diff --git a/tests/unit/scheduler/test_objects.py b/tests/unit/scheduler/test_objects.py index fc5610fd..2fc4c86f 100644 --- a/tests/unit/scheduler/test_objects.py +++ b/tests/unit/scheduler/test_objects.py @@ -3,6 +3,7 @@ import inspect import typing from collections.abc import AsyncIterator +from types import UnionType from typing import Any, Literal, Optional, TypeVar, Union import pytest @@ -62,8 +63,7 @@ def test_multi_turn_request_t(): assert MultiTurnRequestT.__name__ == "MultiTurnRequestT" value = MultiTurnRequestT.__value__ - assert hasattr(value, "__origin__") - assert value.__origin__ is Union + assert isinstance(value, UnionType) type_params = getattr(MultiTurnRequestT, "__type_params__", ()) assert len(type_params) == 1 @@ -340,7 +340,7 @@ def test_class_signatures(self): for key in self.CHECK_KEYS: assert key in fields field_info = fields[key] - assert field_info.annotation in (Union[float, None], Optional[float]) + assert field_info.annotation in (Union[float, None], Optional[float]) # noqa: UP007 assert field_info.default is None @pytest.mark.smoke @@ -453,7 +453,7 @@ def test_class_signatures(self): for key in self.CHECK_KEYS: assert key in fields field_info = fields[key] - assert field_info.annotation in (Union[float, None], Optional[float]) + assert field_info.annotation in (Union[float, None], Optional[float]) # noqa: UP007 assert field_info.default is None @pytest.mark.smoke @@ -704,11 +704,11 @@ def test_marshalling(self, valid_instances): else: assert original_value is None or isinstance( original_value, - (RequestSchedulerTimings, MeasuredRequestTimings), + RequestSchedulerTimings | MeasuredRequestTimings, ) assert reconstructed_value is None or isinstance( reconstructed_value, - (RequestSchedulerTimings, MeasuredRequestTimings), + RequestSchedulerTimings | MeasuredRequestTimings, ) else: assert original_value == reconstructed_value diff --git a/tests/unit/scheduler/test_scheduler.py b/tests/unit/scheduler/test_scheduler.py index 33efc27f..407dab6c 100644 --- a/tests/unit/scheduler/test_scheduler.py +++ b/tests/unit/scheduler/test_scheduler.py @@ -4,7 +4,6 @@ import inspect import random import uuid -from functools import wraps from typing import Any, Generic import pytest @@ -20,19 +19,7 @@ SynchronousStrategy, ) from guidellm.utils.singleton import ThreadSafeSingletonMixin - - -def async_timeout(delay: float): - """Decorator to add timeout to async test functions.""" - - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout class MockRequest(BaseModel): diff --git a/tests/unit/scheduler/test_strategies.py b/tests/unit/scheduler/test_strategies.py index 67a2d77d..143a3130 100644 --- a/tests/unit/scheduler/test_strategies.py +++ b/tests/unit/scheduler/test_strategies.py @@ -225,7 +225,7 @@ def test_lifecycle( for index in range(max(5, startup_requests + 2)): offset = instance.next_offset() - assert isinstance(offset, (int, float)) + assert isinstance(offset, int | float) if index < startup_requests: expected_offset = initial_offset + (index + 1) * startup_delay diff --git a/tests/unit/scheduler/test_worker.py b/tests/unit/scheduler/test_worker.py index b62d66d5..b6624483 100644 --- a/tests/unit/scheduler/test_worker.py +++ b/tests/unit/scheduler/test_worker.py @@ -5,7 +5,6 @@ import random import time from dataclasses import dataclass -from functools import wraps from multiprocessing import Barrier, Event, Process from multiprocessing.synchronize import Barrier as ProcessingBarrier from multiprocessing.synchronize import Event as ProcessingEvent @@ -27,21 +26,11 @@ WorkerProcess, ) from guidellm.utils import InterProcessMessagingQueue +from tests.unit.testing_utils import async_timeout STANDARD_NUM_REQUESTS: int = 200 -def async_timeout(delay): - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator - - @dataclass class TimingsBounds: exact: float | None = None diff --git a/tests/unit/scheduler/test_worker_group.py b/tests/unit/scheduler/test_worker_group.py index 80bb6c23..2b8176e7 100644 --- a/tests/unit/scheduler/test_worker_group.py +++ b/tests/unit/scheduler/test_worker_group.py @@ -3,7 +3,6 @@ import asyncio import inspect import time -from functools import wraps from multiprocessing.context import BaseContext from multiprocessing.managers import BaseManager from multiprocessing.process import BaseProcess @@ -30,17 +29,7 @@ ) from guidellm.scheduler.worker_group import WorkerGroupState from guidellm.utils import InterProcessMessaging - - -def async_timeout(delay): - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout class MockRequestTimings(MeasuredRequestTimings): diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 42c8901d..f862684c 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -21,7 +21,7 @@ def test_default_settings(): assert settings.openai == OpenAISettings() assert ( settings.report_generation.source - == "https://blog.vllm.ai/guidellm/ui/latest/index.html" + == "https://blog.vllm.ai/guidellm/ui/v0.3.0/index.html" ) @@ -60,13 +60,13 @@ def test_report_generation_default_source(): settings = Settings(env=Environment.STAGING) assert ( settings.report_generation.source - == "https://blog.vllm.ai/guidellm/ui/release/latest/index.html" + == "https://blog.vllm.ai/guidellm/ui/release/v0.3.0/index.html" ) settings = Settings(env=Environment.PROD) assert ( settings.report_generation.source - == "https://blog.vllm.ai/guidellm/ui/latest/index.html" + == "https://blog.vllm.ai/guidellm/ui/v0.3.0/index.html" ) diff --git a/tests/unit/testing_utils.py b/tests/unit/testing_utils.py new file mode 100644 index 00000000..c6b8c513 --- /dev/null +++ b/tests/unit/testing_utils.py @@ -0,0 +1,41 @@ +"""Common test utilities for async testing.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from functools import wraps +from typing import Any, TypeVar + +import pytest + +# Type variables for proper typing +F = TypeVar("F", bound=Callable[..., Awaitable[Any]]) + + +def async_timeout(delay: float = 10.0, hard_fail: bool = False) -> Callable[[F], F]: + """ + Decorator to add timeout to async test functions. + + Args: + delay: Timeout in seconds (default: 10.0) + + Returns: + Decorated function with timeout applied + """ + + def decorator(func: F) -> F: + @wraps(func) + async def wrapper(*args: Any, **kwargs: Any) -> Any: + try: + return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) + except asyncio.TimeoutError: + msg = f"Test {func.__name__} timed out after {delay} seconds" + if hard_fail: + pytest.fail(msg) + else: + pytest.xfail(msg) + + return wrapper # type: ignore[return-value] + + return decorator diff --git a/tests/unit/utils/test_encoding.py b/tests/unit/utils/test_encoding.py index cc4600cf..cfdf14e2 100644 --- a/tests/unit/utils/test_encoding.py +++ b/tests/unit/utils/test_encoding.py @@ -6,11 +6,11 @@ import pytest from pydantic import BaseModel, Field -from guidellm.backends.objects import ( +from guidellm.scheduler.schemas import RequestSchedulerTimings, ScheduledRequestInfo +from guidellm.schemas.response import ( GenerationRequest, GenerationResponse, ) -from guidellm.scheduler.objects import RequestSchedulerTimings, ScheduledRequestInfo from guidellm.utils.encoding import Encoder, MessageEncoding, Serializer @@ -476,7 +476,7 @@ def test_to_from_sequence_collections(self, collection): seq = inst.to_sequence(collection) out = inst.from_sequence(seq) assert len(out) == len(collection) - assert all(a == b for a, b in zip(out, list(collection))) + assert all(a == b for a, b in zip(out, list(collection), strict=False)) @pytest.mark.sanity def test_to_from_sequence_mapping(self): diff --git a/tests/unit/utils/test_functions.py b/tests/unit/utils/test_functions.py index 3b353759..96b7b920 100644 --- a/tests/unit/utils/test_functions.py +++ b/tests/unit/utils/test_functions.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time from datetime import datetime import pytest @@ -180,6 +181,18 @@ def test_single_value(self): assert result == 3.0 +@pytest.fixture(autouse=True) +def force_us_eastern_timezone(monkeypatch): + """ + Forces the timezone to US/Eastern for the duration of a test. + This ensures that timestamp formatting is consistent across all environments. + + ## WRITTEN BY AI ## + """ + monkeypatch.setenv("TZ", "America/New_York") + time.tzset() # Propagates the change to the underlying C library + + class TestSafeFormatTimestamp: """Test suite for safe_format_timestamp function.""" diff --git a/tests/unit/utils/test_messaging.py b/tests/unit/utils/test_messaging.py index d6b3283d..7b021aa6 100644 --- a/tests/unit/utils/test_messaging.py +++ b/tests/unit/utils/test_messaging.py @@ -3,7 +3,6 @@ import asyncio import multiprocessing import threading -from functools import wraps from typing import Any, TypeVar import culsans @@ -22,19 +21,7 @@ InterProcessMessagingQueue, ) from guidellm.utils.messaging import ReceiveMessageT, SendMessageT - - -def async_timeout(delay: float): - """Decorator to add timeout to async test functions.""" - - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout class MockMessage(BaseModel): diff --git a/tests/unit/utils/test_synchronous.py b/tests/unit/utils/test_synchronous.py index 1a9ea2c9..eebd6a52 100644 --- a/tests/unit/utils/test_synchronous.py +++ b/tests/unit/utils/test_synchronous.py @@ -3,10 +3,9 @@ import asyncio import multiprocessing import threading -from functools import wraps from multiprocessing.synchronize import Barrier as ProcessingBarrier from multiprocessing.synchronize import Event as ProcessingEvent -from typing import Union +from typing import get_args import pytest @@ -16,33 +15,29 @@ wait_for_sync_event, wait_for_sync_objects, ) +from tests.unit.testing_utils import async_timeout -def async_timeout(delay: float): - """Decorator to add timeout to async functions.""" - - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func +def test_sync_object_types_alias(): + """ + Test that SyncObjectTypesAlias is defined correctly as a type alias. - return decorator + ## WRITTEN BY AI ## + """ + # Get the actual types from the union alias + actual_types = get_args(SyncObjectTypesAlias) + # Define the set of expected types + expected_types = { + threading.Event, + ProcessingEvent, + threading.Barrier, + ProcessingBarrier, + } -def test_sync_object_types_alias(): - """Test that SyncObjectTypesAlias is defined correctly as a type alias.""" - assert hasattr(SyncObjectTypesAlias, "__origin__") - if hasattr(SyncObjectTypesAlias, "__args__"): - actual_type = SyncObjectTypesAlias.__args__[0] - assert hasattr(actual_type, "__origin__") - assert actual_type.__origin__ is Union - union_args = actual_type.__args__ - assert threading.Event in union_args - assert ProcessingEvent in union_args - assert threading.Barrier in union_args - assert ProcessingBarrier in union_args + # Assert that the set of actual types matches the expected set. + # Using a set comparison is robust as it ignores the order. + assert set(actual_types) == expected_types class TestWaitForSyncEvent: @@ -226,7 +221,7 @@ async def test_invocation(self, objects_types, expected_result): async def set_target(): await asyncio.sleep(0.01) obj = objects[expected_result] - if isinstance(obj, (threading.Event, ProcessingEvent)): + if isinstance(obj, threading.Event | ProcessingEvent): obj.set() else: await asyncio.to_thread(obj.wait) diff --git a/tests/unit/utils/test_typing.py b/tests/unit/utils/test_typing.py index fafa8765..1e31ef8e 100644 --- a/tests/unit/utils/test_typing.py +++ b/tests/unit/utils/test_typing.py @@ -2,10 +2,9 @@ Test suite for the typing utilities module. """ -from typing import Annotated, Literal, Union +from typing import Annotated, Literal, TypeAlias import pytest -from typing_extensions import TypeAlias from guidellm.utils.typing import get_literal_vals @@ -15,7 +14,7 @@ Literal["synchronous", "concurrent", "throughput", "constant", "poisson"], "Valid strategy type identifiers for scheduling request patterns", ] -StrategyProfileType: TypeAlias = Union[LocalStrategyType, LocalProfileType] +StrategyProfileType: TypeAlias = LocalStrategyType | LocalProfileType class TestGetLiteralVals: @@ -54,7 +53,7 @@ def test_inline_union_type(self): ### WRITTEN BY AI ### """ - result = get_literal_vals(Union[LocalProfileType, LocalStrategyType]) + result = get_literal_vals(LocalProfileType | LocalStrategyType) expected = frozenset( { "synchronous", @@ -118,6 +117,6 @@ def test_literal_union(self): ### WRITTEN BY AI ### """ - result = get_literal_vals(Union[Literal["test", "test2"], Literal["test3"]]) + result = get_literal_vals(Literal["test", "test2"] | Literal["test3"]) expected = frozenset({"test", "test2", "test3"}) assert result == expected diff --git a/tox.ini b/tox.ini index 723d9382..224e5ef3 100644 --- a/tox.ini +++ b/tox.ini @@ -58,7 +58,7 @@ description = Run type checks deps = .[dev] commands = - mypy --check-untyped-defs + mypy --check-untyped-defs {posargs} [testenv:links] @@ -98,3 +98,13 @@ commands = rm -rf .tox rm -rf .ruff_cache rm -rf .coverage + + +[testenv:lock] +description = Update pylock +skip_install = true +setenv = + PDM_IGNORE_SAVED_PYTHON="1" +deps = pdm[all] +commands = + pdm lock --update-reuse